Repository: VKCOM/noverify
Pull request #1128 (2021-08-29 11:43:19)

linter: move checks from `rootIndexer` to `rootChecker`
#783 


Patch:
diff --git a/src/linter/block.go b/src/linter/block.go
index 7b4cab1..e0782dd 100644
--- a/src/linter/block.go
+++ b/src/linter/block.go
@@ -120,7 +120,7 @@ func newBlockWalker(r *rootWalker, sc *meta.Scope) *blockWalker {
 		nonLocalVars: make(map[string]variableKind),
 		path:         irutil.NewNodePath(),
 	}
-	b.linter = blockLinter{walker: b}
+	b.linter = blockLinter{walker: b, quickfix: r.checker.quickfix}
 	return b
 }
 
@@ -266,7 +266,7 @@ func (b *blockWalker) handleCommentToken(n ir.Node, t *token.Token) {
 		typesMap := types.NewMapWithNormalization(b.r.ctx.typeNormalizer, converted.Types)
 		b.ctx.sc.AddVarFromPHPDoc(strings.TrimPrefix(part.Var, "$"), typesMap, "@var")
 
-		b.r.checkUndefinedClassesInPHPDoc(n, typesMap, part)
+		b.r.checker.checkUndefinedClassesInPHPDoc(n, typesMap, part)
 	}
 }
 
@@ -1187,17 +1187,16 @@ func (b *blockWalker) enterArrowFunction(fun *ir.ArrowFunctionExpr) bool {
 	moveShapesToContext(&b.r.ctx, doc.Shapes)
 	b.r.handleClosuresFromDoc(doc.Closures)
 
-	for _, param := range fun.Params {
-		b.r.checkFuncParam(param.(*ir.Parameter))
-	}
+	funcParams := b.r.parseFuncParams(fun.Params, doc.ParamTypes, sc, nil)
+	b.r.handleArrowFuncExpr(funcParams.params, fun.Expr, sc, b)
+
+	name := &ir.Identifier{Value: "arrow function"}
+	b.r.checker.CheckFuncParams(name, fun.Params, funcParams, doc.ParamTypes)
 
 	// Check stage.
-	errors := b.r.checkPHPDoc(fun, fun.Doc, fun.Params)
+	errors := b.r.checker.CheckPHPDoc(fun, fun.Doc, fun.Params)
 	b.r.reportPHPDocErrors(errors)
 
-	funcParams := b.r.parseFuncParams(fun.Params, doc.ParamTypes, sc, nil)
-	b.r.handleArrowFuncExpr(funcParams.params, fun.Expr, sc, b)
-
 	return false
 }
 
@@ -1217,14 +1216,14 @@ func (b *blockWalker) enterClosure(fun *ir.ClosureExpr, haveThis bool, thisType
 	b.r.handleClosuresFromDoc(doc.Closures)
 
 	// Check stage.
-	errors := b.r.checkPHPDoc(fun, fun.Doc, fun.Params)
+	errors := b.r.checker.CheckPHPDoc(fun, fun.Doc, fun.Params)
 	b.r.reportPHPDocErrors(errors)
 
 	var hintReturnType types.Map
 	if typ, ok := b.r.parseTypeHintNode(fun.ReturnType); ok {
 		hintReturnType = typ
 	}
-	b.r.checkTypeHintNode(fun.ReturnType, "closure return type")
+	b.r.checker.CheckTypeHintNode(fun.ReturnType, "closure return type")
 
 	var closureUses []ir.Node
 	if fun.ClosureUse != nil {
@@ -1264,12 +1263,10 @@ func (b *blockWalker) enterClosure(fun *ir.ClosureExpr, haveThis bool, thisType
 
 	returnTypes := functionReturnType(phpDocReturnTypes, hintReturnType, actualReturnTypes)
 
-	for _, param := range fun.Params {
-		b.r.checkFuncParam(param.(*ir.Parameter))
-	}
-
 	name := autogen.GenerateClosureName(fun, b.r.ctx.st.CurrentFunction, b.r.ctx.st.CurrentFile)
 
+	b.r.checker.CheckFuncParams(&ir.Identifier{Value: name}, fun.Params, params, doc.ParamTypes)
+
 	if b.r.meta.Functions.H == nil {
 		b.r.meta.Functions = meta.NewFunctionsMap()
 	}
@@ -1703,7 +1700,7 @@ func moveNonLocalVariables(trueContext, toContext *blockContext, nonLocalVars ma
 }
 
 func (b *blockWalker) handleElseIf(s *ir.ElseIfStmt) {
-	b.r.checkKeywordCase(s, "elseif")
+	b.r.checker.CheckKeywordCase(s, "elseif")
 }
 
 func (b *blockWalker) iterateNextCases(cases []ir.Node, startIdx int) {
@@ -1745,10 +1742,10 @@ func (b *blockWalker) handleSwitch(s *ir.SwitchStmt) bool {
 		cond, list := getCaseStmts(c)
 		if cond == nil {
 			haveDefault = true
-			b.r.checkKeywordCase(c, "default")
+			b.r.checker.CheckKeywordCase(c, "default")
 		} else {
 			cond.Walk(b)
-			b.r.checkKeywordCase(c, "case")
+			b.r.checker.CheckKeywordCase(c, "case")
 		}
 
 		// allow empty case body without "break;"
diff --git a/src/linter/block_linter.go b/src/linter/block_linter.go
index 3ad5f17..b287730 100644
--- a/src/linter/block_linter.go
+++ b/src/linter/block_linter.go
@@ -17,7 +17,8 @@ import (
 )
 
 type blockLinter struct {
-	walker *blockWalker
+	walker   *blockWalker
+	quickfix *QuickFixGenerator
 }
 
 func (b *blockLinter) enterNode(n ir.Node) {
@@ -143,34 +144,34 @@ func (b *blockLinter) enterNode(n ir.Node) {
 		b.checkTypeCaseExpr(n)
 
 	case *ir.CloneExpr:
-		b.walker.r.checkKeywordCase(n, "clone")
+		b.walker.r.checker.CheckKeywordCase(n, "clone")
 	case *ir.ConstListStmt:
-		b.walker.r.checkKeywordCase(n, "const")
+		b.walker.r.checker.CheckKeywordCase(n, "const")
 	case *ir.GotoStmt:
-		b.walker.r.checkKeywordCase(n, "goto")
+		b.walker.r.checker.CheckKeywordCase(n, "goto")
 	case *ir.ThrowStmt:
-		b.walker.r.checkKeywordCase(n, "throw")
+		b.walker.r.checker.CheckKeywordCase(n, "throw")
 	case *ir.YieldExpr:
-		b.walker.r.checkKeywordCase(n, "yield")
+		b.walker.r.checker.CheckKeywordCase(n, "yield")
 	case *ir.YieldFromExpr:
-		b.walker.r.checkKeywordCase(n, "yield")
+		b.walker.r.checker.CheckKeywordCase(n, "yield")
 	case *ir.ImportExpr:
-		b.walker.r.checkKeywordCase(n, n.Func)
+		b.walker.r.checker.CheckKeywordCase(n, n.Func)
 	case *ir.BreakStmt:
-		b.walker.r.checkKeywordCase(n, "break")
+		b.walker.r.checker.CheckKeywordCase(n, "break")
 	case *ir.ReturnStmt:
-		b.walker.r.checkKeywordCase(n, "return")
+		b.walker.r.checker.CheckKeywordCase(n, "return")
 	case *ir.ElseStmt:
-		b.walker.r.checkKeywordCase(n, "else")
+		b.walker.r.checker.CheckKeywordCase(n, "else")
 
 	case *ir.ForeachStmt:
 		b.checkForeach(n)
 	case *ir.ForStmt:
-		b.walker.r.checkKeywordCase(n, "for")
+		b.walker.r.checker.CheckKeywordCase(n, "for")
 	case *ir.WhileStmt:
-		b.walker.r.checkKeywordCase(n, "while")
+		b.walker.r.checker.CheckKeywordCase(n, "while")
 	case *ir.DoStmt:
-		b.walker.r.checkKeywordCase(n, "do")
+		b.walker.r.checker.CheckKeywordCase(n, "do")
 
 	case *ir.ContinueStmt:
 		b.checkContinueStmt(n)
@@ -239,7 +240,7 @@ func (b *blockLinter) checkClass(class *ir.ClassStmt) {
 }
 
 func (b *blockLinter) checkForeach(n *ir.ForeachStmt) {
-	b.walker.r.checkKeywordCase(n, "foreach")
+	b.walker.r.checker.CheckKeywordCase(n, "foreach")
 
 	var vars []*ir.SimpleVar
 
@@ -362,14 +363,14 @@ func (b *blockLinter) checkTryStmt(s *ir.TryStmt) {
 		b.report(s, LevelError, "bareTry", "At least one catch or finally block must be present")
 	}
 
-	b.walker.r.checkKeywordCase(s, "try")
+	b.walker.r.checker.CheckKeywordCase(s, "try")
 
 	for _, c := range s.Catches {
-		b.walker.r.checkKeywordCase(c, "catch")
+		b.walker.r.checker.CheckKeywordCase(c, "catch")
 	}
 
 	if s.Finally != nil {
-		b.walker.r.checkKeywordCase(s.Finally, "finally")
+		b.walker.r.checker.CheckKeywordCase(s.Finally, "finally")
 	}
 
 	if len(s.Catches) > 1 {
@@ -474,7 +475,7 @@ func (b *blockLinter) checkRedundantCast(e ir.Node, dstType string) {
 }
 
 func (b *blockLinter) checkNew(e *ir.NewExpr) {
-	b.walker.r.checkKeywordCase(e, "new")
+	b.walker.r.checker.CheckKeywordCase(e, "new")
 
 	if b.classParseState().IsTrait {
 		switch {
@@ -509,13 +510,13 @@ func (b *blockLinter) checkNew(e *ir.NewExpr) {
 		if ok {
 			b.report(e.Class, LevelError, "invalidNew", "Cannot instantiate trait %s", class.Name)
 		} else {
-			b.walker.r.reportUndefinedClass(e.Class, className)
+			b.walker.r.checker.ReportUndefinedClass(e.Class, className)
 		}
 	} else {
 		if class.IsInterface() {
 			b.report(e.Class, LevelError, "invalidNew", "Cannot instantiate interface %s", class.Name)
 		}
-		b.walker.r.checkNameCase(e.Class, className, class.Name)
+		b.walker.r.checker.CheckNameCase(e.Class, className, class.Name)
 	}
 
 	// It's illegal to instantiate abstract class, but `static` can
@@ -610,7 +611,7 @@ func (b *blockLinter) checkTernary(e *ir.TernaryExpr) {
 }
 
 func (b *blockLinter) checkGlobalStmt(s *ir.GlobalStmt) {
-	b.walker.r.checkKeywordCase(s, "global")
+	b.walker.r.checker.CheckKeywordCase(s, "global")
 
 	for _, v := range s.Vars {
 		v, ok := v.(*ir.SimpleVar)
@@ -781,7 +782,7 @@ func (b *blockLinter) checkIntOverflow(num *ir.Dnumber) {
 }
 
 func (b *blockLinter) checkContinueStmt(c *ir.ContinueStmt) {
-	b.walker.r.checkKeywordCase(c, "continue")
+	b.walker.r.checker.CheckKeywordCase(c, "continue")
 	if c.Expr == nil && b.walker.ctx.innermostLoop == loopSwitch {
 		inLoop := irutil.InLoop(b.walker.path)
 		msg := "Use 'break' instead of 'continue' in switch"
@@ -796,7 +797,7 @@ func (b *blockLinter) checkContinueStmt(c *ir.ContinueStmt) {
 func (b *blockLinter) checkArray(arr *ir.ArrayExpr) {
 	if !arr.ShortSyntax {
 		b.report(arr, LevelNotice, "arraySyntax", "Use the short form '[]' instead of the old 'array()'")
-		b.walker.r.addFixForArray(arr)
+		b.walker.r.addQuickFix("arraySyntax", b.quickfix.Array(arr))
 	}
 
 	multiline := false
@@ -1004,7 +1005,7 @@ func (b *blockLinter) checkFunctionCall(e *ir.FunctionCallExpr) {
 		b.walker.untrackVarName(varName)
 	} else {
 		b.checkFunctionAvailability(e, &call)
-		b.walker.r.checkNameCase(e.Function, call.funcName, call.info.Name)
+		b.walker.r.checker.CheckNameCase(e.Function, call.funcName, call.info.Name)
 	}
 
 	if call.isFound {
@@ -1174,7 +1175,7 @@ func (b *blockLinter) checkMethodCall(e *ir.MethodCallExpr) {
 		}
 	} else if !call.isMagic && !parseState.IsTrait {
 		// Method is defined.
-		b.walker.r.checkNameCase(e.Method, call.methodName, call.info.Name)
+		b.walker.r.checker.CheckNameCase(e.Method, call.methodName, call.info.Name)
 		if call.info.IsStatic() {
 			b.report(e.Method, LevelWarning, "callStatic", "Calling static method as instance method")
 		}
@@ -1278,7 +1279,7 @@ func (b *blockLinter) checkClassConstFetch(e *ir.ClassConstFetchExpr) {
 	if !utils.IsSpecialClassName(e.Class) {
 		usedClassName, ok := solver.GetClassName(b.classParseState(), e.Class)
 		if ok {
-			b.walker.r.checkNameCase(e.Class, usedClassName, fetch.className)
+			b.walker.r.checker.CheckNameCase(e.Class, usedClassName, fetch.className)
 		}
 	}
 
diff --git a/src/linter/quickfix.go b/src/linter/quickfix.go
new file mode 100644
index 0000000..1f937e6
--- /dev/null
+++ b/src/linter/quickfix.go
@@ -0,0 +1,48 @@
+package linter
+
+import (
+	"bytes"
+	"fmt"
+
+	"github.com/VKCOM/noverify/src/ir"
+	"github.com/VKCOM/noverify/src/quickfix"
+	"github.com/VKCOM/noverify/src/workspace"
+)
+
+type QuickFixGenerator struct {
+	file *workspace.File
+}
+
+func NewQuickFixGenerator(file *workspace.File) *QuickFixGenerator {
+	return &QuickFixGenerator{file: file}
+}
+
+func (g *QuickFixGenerator) Array(arr *ir.ArrayExpr) quickfix.TextEdit {
+	from := arr.Position.StartPos
+	to := arr.Position.EndPos
+
+	have := g.file.Contents()[from:to]
+	have = bytes.TrimPrefix(have, []byte("array"))
+	have = bytes.TrimSpace(have)
+	have = bytes.TrimPrefix(have, []byte("("))
+	have = bytes.TrimSuffix(have, []byte(")"))
+
+	return quickfix.TextEdit{
+		StartPos:    arr.Position.StartPos,
+		EndPos:      arr.Position.EndPos,
+		Replacement: fmt.Sprintf("[%s]", string(have)),
+	}
+}
+
+func (g *QuickFixGenerator) NullForNotNullableProperty(prop *ir.PropertyStmt) quickfix.TextEdit {
+	from := prop.Position.StartPos
+	to := prop.Variable.Position.EndPos
+
+	withoutAssign := g.file.Contents()[from:to]
+
+	return quickfix.TextEdit{
+		StartPos:    prop.Position.StartPos,
+		EndPos:      prop.Position.EndPos,
+		Replacement: string(withoutAssign),
+	}
+}
diff --git a/src/linter/root.go b/src/linter/root.go
index 50b0c9c..e7351df 100644
--- a/src/linter/root.go
+++ b/src/linter/root.go
@@ -75,6 +75,8 @@ type rootWalker struct {
 	config *Config
 
 	checkersFilter *CheckersFilter
+
+	checker *rootChecker
 }
 
 // InitCustom is needed to initialize walker state
@@ -86,6 +88,8 @@ func (d *rootWalker) InitCustom() {
 	d.customBlock = append(d.customBlock, d.config.Checkers.blockCheckers...)
 }
 
+// Getters part.
+
 // scope returns root-level variable scope if applicable.
 func (d *rootWalker) scope() *meta.Scope {
 	if d.meta.Scope == nil {
@@ -94,6 +98,11 @@ func (d *rootWalker) scope() *meta.Scope {
 	return d.meta.Scope
 }
 
+// metaInfo returns meta info.
+func (d *rootWalker) metaInfo() *meta.Info {
+	return d.ctx.st.Info
+}
+
 // state allows for custom hooks to store state between entering root context and block context.
 func (d *rootWalker) state() map[string]interface{} {
 	if d.customState == nil {
@@ -107,51 +116,9 @@ func (d *rootWalker) File() *workspace.File {
 	return d.file
 }
 
-func (d *rootWalker) handleCommentToken(t *token.Token) bool {
-	if !phpdoc.IsPHPDocToken(t) {
-		return true
-	}
-
-	for _, ln := range phpdoc.Parse(d.ctx.phpdocTypeParser, string(t.Value)).Parsed {
-		if ln.Name() != "linter" {
-			continue
-		}
-
-		for _, p := range ln.(*phpdoc.RawCommentPart).Params {
-			if p != "disable" {
-				continue
-			}
-			if d.linterDisabled {
-				needleLine := ln.Line() + t.Position.StartLine - 1 - 1
-				d.ReportPHPDoc(
-					PHPDocAbsoluteLineField(needleLine, 1),
-					LevelWarning, "linterError", "Linter is already disabled for this file",
-				)
-				continue
-			}
-			canDisable := false
-			if d.allowDisabledRegexp != nil {
-				canDisable = d.allowDisabledRegexp.MatchString(d.ctx.st.CurrentFile)
-			}
-			d.linterDisabled = canDisable
-			if !canDisable {
-				needleLine := ln.Line() + t.Position.StartLine - 1 - 1
-				d.ReportPHPDoc(
-					PHPDocAbsoluteLineField(needleLine, 1),
-					LevelWarning, "linterError", "You are not allowed to disable linter",
-				)
-			}
-		}
-	}
-
-	return true
-}
-
-func (d *rootWalker) handleComments(n ir.Node) {
-	n.IterateTokens(d.handleCommentToken)
-}
+// Visitor part.
 
-// EnterNode is invoked at every node in hierarchy
+// EnterNode is invoked at every node in hierarchy.
 func (d *rootWalker) EnterNode(n ir.Node) (res bool) {
 	res = true
 
@@ -159,7 +126,7 @@ func (d *rootWalker) EnterNode(n ir.Node) (res bool) {
 		c.BeforeEnterNode(n)
 	}
 
-	d.handleComments(n)
+	n.IterateTokens(d.handleCommentToken)
 
 	state.EnterNode(d.ctx.st, n)
 
@@ -184,11 +151,11 @@ func (d *rootWalker) EnterNode(n ir.Node) (res bool) {
 		cl := d.getClass()
 		className := &ir.Identifier{Value: cl.Name}
 
-		d.checkImplements(n, n.Implements, cl)
-		d.checkExtends(n, n.Extends)
+		d.checker.CheckImplements(n, cl, n.Implements)
+		d.checker.CheckExtends(n, cl, n.Extends)
 
-		d.checkCommentMisspellings(className, n.Doc.Raw)
-		d.checkIdentMisspellings(className)
+		d.checker.CheckCommentMisspellings(className, n.Doc.Raw)
+		d.checker.CheckIdentMisspellings(className)
 
 		doc := d.parseClassPHPDoc(className, n.Doc)
 		d.reportPHPDocErrors(doc.errs)
@@ -198,10 +165,10 @@ func (d *rootWalker) EnterNode(n ir.Node) (res bool) {
 
 	case *ir.InterfaceStmt:
 		d.currentClassNodeStack.Push(n)
-		d.checkKeywordCase(n, "interface")
-		d.checkCommentMisspellings(n.InterfaceName, n.Doc.Raw)
+		d.checker.CheckKeywordCase(n, "interface")
+		d.checker.CheckCommentMisspellings(n.InterfaceName, n.Doc.Raw)
 		if !strings.HasSuffix(n.InterfaceName.Value, "able") {
-			d.checkIdentMisspellings(n.InterfaceName)
+			d.checker.CheckIdentMisspellings(n.InterfaceName)
 		}
 	case *ir.ClassStmt:
 		d.currentClassNodeStack.Push(n)
@@ -218,14 +185,14 @@ func (d *rootWalker) EnterNode(n ir.Node) (res bool) {
 		}
 
 		for _, m := range n.Modifiers {
-			d.lowerCaseModifier(m)
+			d.checker.CheckModifierKeywordCase(m)
 		}
 
-		d.checkImplements(n, n.Implements, cl)
-		d.checkExtends(n, n.Extends)
+		d.checker.CheckImplements(n, cl, n.Implements)
+		d.checker.CheckExtends(n, cl, n.Extends)
 
-		d.checkCommentMisspellings(n.ClassName, n.Doc.Raw)
-		d.checkIdentMisspellings(n.ClassName)
+		d.checker.CheckCommentMisspellings(n.ClassName, n.Doc.Raw)
+		d.checker.CheckIdentMisspellings(n.ClassName)
 
 		doc := d.parseClassPHPDoc(n, n.Doc)
 		d.reportPHPDocErrors(doc.errs)
@@ -235,17 +202,17 @@ func (d *rootWalker) EnterNode(n ir.Node) (res bool) {
 
 	case *ir.TraitStmt:
 		d.currentClassNodeStack.Push(n)
-		d.checkKeywordCase(n, "trait")
-		d.checkCommentMisspellings(n.TraitName, n.Doc.Raw)
-		d.checkIdentMisspellings(n.TraitName)
+		d.checker.CheckKeywordCase(n, "trait")
+		d.checker.CheckCommentMisspellings(n.TraitName, n.Doc.Raw)
+		d.checker.CheckIdentMisspellings(n.TraitName)
 	case *ir.TraitUseStmt:
-		d.checkKeywordCase(n, "use")
+		d.checker.CheckKeywordCase(n, "use")
 		cl := d.getClass()
 		for _, tr := range n.Traits {
 			traitName, ok := solver.GetClassName(d.ctx.st, tr)
 			if ok {
 				cl.Traits[traitName] = struct{}{}
-				d.checkTraitImplemented(d.currentClassNodeStack.Current(), tr, traitName)
+				d.checker.CheckTraitImplemented(d.currentClassNodeStack.Current(), tr, d.getClass(), traitName)
 			}
 		}
 	case *ir.Assign:
@@ -257,7 +224,7 @@ func (d *rootWalker) EnterNode(n ir.Node) (res bool) {
 		d.scope().AddVar(v, solver.ExprTypeLocal(d.scope(), d.ctx.st, n.Expr), "global variable", meta.VarAlwaysDefined)
 	case *ir.FunctionStmt:
 		res = d.enterFunction(n)
-		d.checkKeywordCase(n, "function")
+		d.checker.CheckKeywordCase(n, "function")
 	case *ir.PropertyListStmt:
 		res = d.enterPropertyList(n)
 	case *ir.ClassConstListStmt:
@@ -270,7 +237,7 @@ func (d *rootWalker) EnterNode(n ir.Node) (res bool) {
 		res = d.enterConstList(n)
 
 	case *ir.NamespaceStmt:
-		d.checkKeywordCase(n, "namespace")
+		d.checker.CheckKeywordCase(n, "namespace")
 	}
 
 	for _, c := range d.custom {
@@ -292,391 +259,142 @@ func (d *rootWalker) EnterNode(n ir.Node) (res bool) {
 	return res
 }
 
-func (d *rootWalker) handleClassDoc(doc classPHPDocParseResult, cl *meta.ClassInfo) {
-	// If we ever need to distinguish @property-annotated and real properties,
-	// more work will be required here.
-	for name, p := range doc.properties {
-		p.Pos = cl.Pos
-		cl.Properties[name] = p
-	}
-
-	for name, m := range doc.methods.H {
-		m.Pos = cl.Pos
-		cl.Methods.H[name] = m
+// LeaveNode is invoked after node process.
+func (d *rootWalker) LeaveNode(n ir.Node) {
+	for _, c := range d.custom {
+		c.BeforeLeaveNode(n)
 	}
 
-	cl.Mixins = doc.mixins
-}
+	switch n.(type) {
+	case *ir.ClassStmt, *ir.InterfaceStmt, *ir.TraitStmt, *ir.AnonClassExpr:
+		d.getClass() // populate classes map
 
-func (d *rootWalker) checkExtends(class ir.Node, extends *ir.ClassExtendsStmt) {
-	if extends == nil {
-		return
+		d.currentClassNodeStack.Pop()
 	}
 
-	d.checkKeywordCase(extends, "extends")
+	state.LeaveNode(d.ctx.st, n)
 
-	className, ok := solver.GetClassName(d.ctx.st, extends.ClassName)
-	if ok {
-		d.checkClassInherit(class, extends.ClassName, className)
+	for _, c := range d.custom {
+		c.AfterLeaveNode(n)
 	}
 }
 
-func (d *rootWalker) checkImplements(class ir.Node, implements *ir.ClassImplementsStmt, cl meta.ClassInfo) {
-	if implements == nil {
-		return
-	}
-
-	d.checkKeywordCase(implements, "implements")
-
-	for _, tr := range implements.InterfaceNames {
-		interfaceName, ok := solver.GetClassName(d.ctx.st, tr)
-		if ok {
-			cl.Interfaces[interfaceName] = struct{}{}
-			d.checkIfaceImplemented(class, tr, interfaceName)
-		}
+// beforeEnterFile is invoked before file process.
+func (d *rootWalker) beforeEnterFile() {
+	for _, c := range d.custom {
+		c.BeforeEnterFile()
 	}
 }
 
-func (d *rootWalker) getClassModifiers(n *ir.ClassStmt) meta.ClassFlags {
-	var classFlags meta.ClassFlags
-	for _, m := range n.Modifiers {
-		switch {
-		case strings.EqualFold("abstract", m.Value):
-			classFlags |= meta.ClassAbstract
-		case strings.EqualFold("final", m.Value):
-			classFlags |= meta.ClassFinal
-		}
+// afterLeaveFile is invoked after file process.
+func (d *rootWalker) afterLeaveFile() {
+	for _, c := range d.custom {
+		c.AfterLeaveFile()
 	}
-	return classFlags
-}
 
-func (d *rootWalker) parseStartPos(pos *position.Position) (startLn []byte, startChar int) {
-	if pos.StartLine >= 1 && d.file.NumLines() > pos.StartLine {
-		startLn = d.file.Line(pos.StartLine - 1)
-		p := d.file.LinePosition(pos.StartLine - 1)
-		if pos.StartPos > p {
-			startChar = pos.StartPos - p
+	if !d.metaInfo().IsIndexingComplete() {
+		for _, shape := range d.ctx.shapes {
+			props := make(meta.PropertiesMap)
+			for _, p := range shape.Props {
+				props[p.Key] = meta.PropertyInfo{
+					Typ:         types.NewMapWithNormalization(d.ctx.typeNormalizer, p.Types).Immutable(),
+					AccessLevel: meta.Public,
+				}
+			}
+			cl := meta.ClassInfo{
+				Name:       shape.Name,
+				Properties: props,
+				Flags:      meta.ClassShape,
+			}
+			if d.meta.Classes.H == nil {
+				d.meta.Classes = meta.NewClassesMap()
+			}
+			d.meta.Classes.Set(shape.Name, cl)
 		}
 	}
-
-	return startLn, startChar
 }
 
-// ReportPHPDoc registers a single report message about some found problem in PHPDoc.
-func (d *rootWalker) ReportPHPDoc(phpDocLocation PHPDocLocation, level int, checkName, msg string, args ...interface{}) {
-	if phpDocLocation.RelativeLine {
-		doc, ok := irutil.FindPHPDoc(phpDocLocation.Node, true)
-		if !ok {
-			// If PHPDoc for some reason was not found, give a warning to the node.
-			d.Report(phpDocLocation.Node, level, checkName, msg, args...)
-			return
-		}
+// Handle comments part.
 
-		countPHPDocLines := strings.Count(doc, "\n") + 1
+func (d *rootWalker) handleCommentToken(t *token.Token) bool {
+	if !phpdoc.IsPHPDocToken(t) {
+		return true
+	}
 
-		nodePos := ir.GetPosition(phpDocLocation.Node)
-		if nodePos == nil {
-			// If position for some reason was not found, give a warning to the node.
-			d.Report(phpDocLocation.Node, level, checkName, msg, args...)
-			return
+	for _, ln := range phpdoc.Parse(d.ctx.phpdocTypeParser, string(t.Value)).Parsed {
+		if ln.Name() != "linter" {
+			continue
 		}
 
-		// 1| <?php
-		// 2|
-		// 3| /**
-		// 4|  * Comment
-		// 5|  * @param int $a    <- phpDocLocation.Line == 3
-		// 6|  */                 <- countPHPDocLines == 4
-		// 7| function f($a) {}   <- nodePos.StartLine == 7
-		//
-		// countPHPDocLines - phpDocLocation.Line = 1
-		// nodePos.StartLine - 1 = 6
-		// 6 - 1 = 5 (number of the required line relative to one)
-		// 5 - 1 = 4 (number of the required line relative to zero)
-		phpDocLocation.Line = nodePos.StartLine - (countPHPDocLines - phpDocLocation.Line) - 1 - 1
-	}
-
-	if phpDocLocation.Line < 0 || phpDocLocation.Line >= d.file.NumLines() {
-		d.Report(phpDocLocation.Node, level, checkName, msg, args...)
-		return
+		for _, p := range ln.(*phpdoc.RawCommentPart).Params {
+			if p != "disable" {
+				continue
+			}
+			if d.linterDisabled {
+				needleLine := ln.Line() + t.Position.StartLine - 1 - 1
+				d.ReportPHPDoc(
+					PHPDocAbsoluteLineField(needleLine, 1),
+					LevelWarning, "linterError", "Linter is already disabled for this file",
+				)
+				continue
+			}
+			canDisable := false
+			if d.allowDisabledRegexp != nil {
+				canDisable = d.allowDisabledRegexp.MatchString(d.ctx.st.CurrentFile)
+			}
+			d.linterDisabled = canDisable
+			if !canDisable {
+				needleLine := ln.Line() + t.Position.StartLine - 1 - 1
+				d.ReportPHPDoc(
+					PHPDocAbsoluteLineField(needleLine, 1),
+					LevelWarning, "linterError", "You are not allowed to disable linter",
+				)
+			}
+		}
 	}
 
-	contextLine := d.file.Line(phpDocLocation.Line)
+	return true
+}
 
-	lineWithoutBeginning := contextLine
-	// For the case when we give a warning about the wrong start
-	// of PHPDoc (/* instead /**), it is not necessary to delete characters.
-	if !bytes.Contains(contextLine, []byte("/*")) || bytes.Contains(contextLine, []byte("/**")) {
-		lineWithoutBeginning = bytes.TrimLeft(contextLine, "/ *")
-	}
+// Handle functions part.
 
-	shiftFromStart := len(contextLine) - len(lineWithoutBeginning)
+type handleFuncResult struct {
+	returnTypes            types.Map
+	prematureExitFlags     int
+	callsParentConstructor bool
+}
 
-	parts := bytes.Fields(lineWithoutBeginning)
-	if phpDocLocation.Field >= len(parts) {
-		phpDocLocation.Field = 0
-		phpDocLocation.WholeLine = true
+func (d *rootWalker) handleArrowFuncExpr(params []meta.FuncParam, expr ir.Node, sc *meta.Scope, parentBlockWalker *blockWalker) handleFuncResult {
+	b := newBlockWalker(d, sc)
+	for _, createFn := range d.customBlock {
+		b.custom = append(b.custom, createFn(&BlockContext{w: b}))
 	}
 
-	var startChar int
-	var endChar int
-
-	if phpDocLocation.WholeLine {
-		startChar = shiftFromStart
-		endChar = len(contextLine)
-	} else {
-		part := parts[phpDocLocation.Field]
-		shiftStart := bytes.Index(lineWithoutBeginning, part)
-		shiftEnd := shiftStart + len(part)
+	b.inArrowFunction = true
+	parentBlockWalker.parentBlockWalkers = append(parentBlockWalker.parentBlockWalkers, parentBlockWalker)
+	b.parentBlockWalkers = parentBlockWalker.parentBlockWalkers
 
-		startChar = shiftFromStart + shiftStart
-		endChar = shiftFromStart + shiftEnd
+	for _, p := range params {
+		if p.IsRef {
+			b.nonLocalVars[p.Name] = varRef
+		}
 	}
 
-	if endChar == len(contextLine) && bytes.HasSuffix(contextLine, []byte("\r")) {
-		endChar--
-	}
+	b.addStatement(expr)
+	expr.Walk(b)
 
-	loc := ir.Location{
-		StartLine: phpDocLocation.Line,
-		EndLine:   phpDocLocation.Line,
-		StartChar: startChar,
-		EndChar:   endChar,
-	}
+	b.flushUnused()
 
-	d.ReportLocation(loc, level, checkName, msg, args...)
+	return handleFuncResult{
+		returnTypes: b.returnTypes,
+	}
 }
 
-func (d *rootWalker) Report(n ir.Node, level int, checkName, msg string, args ...interface{}) {
-	var pos position.Position
-
-	if n == nil {
-		// Hack to parse syntax error message from php-parser.
-		if strings.Contains(msg, "syntax error") && strings.Contains(msg, " at line ") {
-			// it is in form "Syntax error: syntax error: unexpected '*' at line 4"
-			if lastIdx := strings.LastIndexByte(msg, ' '); lastIdx > 0 {
-				lineNumStr := msg[lastIdx+1:]
-				lineNum, err := strconv.Atoi(lineNumStr)
-				if err == nil {
-					pos.StartLine = lineNum
-					pos.EndLine = lineNum
-					msg = msg[0:lastIdx]
-					msg = strings.TrimSuffix(msg, " at line")
-				}
-			}
-		}
-	} else {
-		nodePos := ir.GetPosition(n)
-		if nodePos == nil {
-			return
-		}
-		pos = *nodePos
-	}
-
-	var loc ir.Location
-
-	loc.StartLine = pos.StartLine - 1
-	loc.EndLine = pos.EndLine - 1
-	loc.StartChar = pos.StartPos
-	loc.EndChar = pos.EndPos
-
-	if pos.StartLine >= 1 && d.file.NumLines() >= pos.StartLine {
-		p := d.file.LinePosition(pos.StartLine - 1)
-		if pos.StartPos >= p {
-			loc.StartChar = pos.StartPos - p
-		}
-	}
-
-	if pos.EndLine >= 1 && d.file.NumLines() >= pos.EndLine {
-		p := d.file.LinePosition(pos.EndLine - 1)
-		if pos.EndPos >= p {
-			loc.EndChar = pos.EndPos - p
-		}
-	}
-
-	d.ReportLocation(loc, level, checkName, msg, args...)
-}
-
-func (d *rootWalker) ReportLocation(loc ir.Location, level int, checkName, msg string, args ...interface{}) {
-	if !d.metaInfo().IsIndexingComplete() {
-		return
-	}
-	if d.file.AutoGenerated() && !d.config.CheckAutoGenerated {
-		return
-	}
-	// We don't report anything if linter was disabled by a
-	// successful @linter disable, unless it's the linterError.
-	if d.linterDisabled && checkName != "linterError" {
-		return
-	}
-
-	if !d.checkersFilter.IsEnabledReport(checkName, d.ctx.st.CurrentFile) {
-		return
-	}
-
-	if loc.StartLine < 0 || loc.StartLine >= d.file.NumLines() {
-		return
-	}
-
-	contextLine := d.file.Line(loc.StartLine)
-
-	var hash uint64
-	// If baseline is not enabled, don't waste time on hash computations.
-	if d.config.ComputeBaselineHashes {
-		hash = d.reportHash(&loc, contextLine, checkName, msg)
-		if count := d.ctx.baseline.Count(hash); count >= 1 {
-			if d.ctx.hashCounters == nil {
-				d.ctx.hashCounters = make(map[uint64]int)
-			}
-			d.ctx.hashCounters[hash]++
-			if d.ctx.hashCounters[hash] <= count {
-				return
-			}
-		}
-	}
-
-	d.reports = append(d.reports, &Report{
-		CheckName: checkName,
-		Context:   string(contextLine),
-		StartChar: loc.StartChar,
-		EndChar:   loc.EndChar,
-		Line:      loc.StartLine + 1,
-		Level:     level,
-		Filename:  strings.ReplaceAll(d.ctx.st.CurrentFile, "\\", "/"), // To make output stable between platforms, see #572
-		Message:   fmt.Sprintf(msg, args...),
-		Hash:      hash,
-	})
-}
-
-// reportHash computes the ReportLocation signature hash for the baseline.
-func (d *rootWalker) reportHash(loc *ir.Location, contextLine []byte, checkName, msg string) uint64 {
-	// Since we store class::method scope, renaming a class would cause baseline
-	// invalidation for the entire class. But this is not an issue, since in
-	// a modern PHP class name always should map to a filename.
-	// If we renamed a class, we probably renamed the file as well, so
-	// the baseline would be invalidated anyway.
-	//
-	// We still get all the benefits by using method prefix: it's common
-	// for different classes to have methods with similar name. We don't
-	// want such methods to be considered as a single "scope".
-	scope := "file"
-	switch {
-	case d.ctx.st.CurrentClass != "" && d.ctx.st.CurrentFunction != "":
-		scope = d.ctx.st.CurrentClass + "::" + d.ctx.st.CurrentFunction
-	case d.ctx.st.CurrentFunction != "":
-		scope = d.ctx.st.CurrentFunction
-	}
-
-	var prevLine []byte
-	var nextLine []byte
-	// Adjacent lines are only included when using non-conservative baseline.
-	if !d.config.ConservativeBaseline {
-		// Lines are 1-based, indexes are 0-based.
-		// If this function is called, we expect that lines[index] exists.
-		index := loc.StartLine - 1
-		if index >= 1 {
-			prevLine = d.file.Line(index - 1)
-		}
-		if d.file.NumLines() > index+1 {
-			nextLine = d.file.Line(index + 1)
-		}
-	}
-
-	// Observation: using a base file name instead of its full name does not
-	// introduce any "bad collisions", because we have a "scope" part
-	// that cuts the risk by a big margin.
-	//
-	// One benefit is that it makes the baseline contents more position-independent.
-	// We don't need to know a source root folder to truncate it.
-	//
-	// Moves like Foo/Bar/User.php -> Common/User.php do not invalidate the
-	// suppress base. This is not an obvious win, but it may be a good
-	// compromise to solve a use case where a class file is being moved.
-	// For classes, filename is unlikely to be changed during this file move operation.
-	//
-	// It can't handle file duplication when Foo/Bar/User.php is *copied* to a new location.
-	// It may be useful to give warnings to both *old* and *new* copies of the duplicated
-	// code since some bugs should be fixed in both places.
-	// We'll see how it goes.
-	filename := filepath.Base(d.ctx.st.CurrentFile)
-
-	d.ctx.scratchBuf.Reset()
-	return baseline.ReportHash(&d.ctx.scratchBuf, baseline.HashFields{
-		Filename:  filename,
-		PrevLine:  bytes.TrimSuffix(prevLine, []byte("\r")),
-		StartLine: bytes.TrimSuffix(contextLine, []byte("\r")),
-		NextLine:  bytes.TrimSuffix(nextLine, []byte("\r")),
-		CheckName: checkName,
-		Message:   msg,
-		Scope:     scope,
-	})
-}
-
-func (d *rootWalker) reportUndefinedVariable(v ir.Node, maybeHave bool, path irutil.NodePath) {
-	sv, ok := v.(*ir.SimpleVar)
-	if !ok {
-		d.Report(v, LevelWarning, "undefinedVariable", "Unknown variable variable %s used",
-			utils.NameNodeToString(v))
-		return
-	}
-
-	if _, ok := superGlobals[sv.Name]; ok {
-		return
-	}
-
-	// For the following cases, we do not give warnings,
-	// as they check for the presence of this variable.
-	// $b = $a ?? 100;
-	// $b = isset($a) ? $a : 100;
-	needWarn := !utils.InCoalesceOrIsset(path)
-	if !needWarn {
-		return
-	}
-
-	if maybeHave {
-		d.Report(sv, LevelWarning, "maybeUndefined", "Possibly undefined variable $%s", sv.Name)
-	} else {
-		d.Report(sv, LevelError, "undefinedVariable", "Cannot find referenced variable $%s", sv.Name)
-	}
-}
-
-type handleFuncResult struct {
-	returnTypes            types.Map
-	prematureExitFlags     int
-	callsParentConstructor bool
-}
-
-func (d *rootWalker) handleArrowFuncExpr(params []meta.FuncParam, expr ir.Node, sc *meta.Scope, parentBlockWalker *blockWalker) handleFuncResult {
-	b := newBlockWalker(d, sc)
-	for _, createFn := range d.customBlock {
-		b.custom = append(b.custom, createFn(&BlockContext{w: b}))
-	}
-
-	b.inArrowFunction = true
-	parentBlockWalker.parentBlockWalkers = append(parentBlockWalker.parentBlockWalkers, parentBlockWalker)
-	b.parentBlockWalkers = parentBlockWalker.parentBlockWalkers
-
-	for _, p := range params {
-		if p.IsRef {
-			b.nonLocalVars[p.Name] = varRef
-		}
-	}
-
-	b.addStatement(expr)
-	expr.Walk(b)
-
-	b.flushUnused()
-
-	return handleFuncResult{
-		returnTypes: b.returnTypes,
-	}
-}
-
-func (d *rootWalker) handleFuncStmts(params []meta.FuncParam, uses, stmts []ir.Node, sc *meta.Scope) handleFuncResult {
-	b := newBlockWalker(d, sc)
-	for _, createFn := range d.customBlock {
-		b.custom = append(b.custom, createFn(&BlockContext{w: b}))
-	}
+func (d *rootWalker) handleFuncStmts(params []meta.FuncParam, uses, stmts []ir.Node, sc *meta.Scope) handleFuncResult {
+	b := newBlockWalker(d, sc)
+	for _, createFn := range d.customBlock {
+		b.custom = append(b.custom, createFn(&BlockContext{w: b}))
+	}
 
 	for _, useExpr := range uses {
 		var byRef bool
@@ -748,313 +466,465 @@ func (d *rootWalker) handleFuncStmts(params []meta.FuncParam, uses, stmts []ir.N
 	}
 }
 
-func (d *rootWalker) checkParentConstructorCall(n ir.Node, parentConstructorCalled bool) {
-	if !d.metaInfo().IsIndexingComplete() {
-		return
-	}
-
-	class, ok := d.currentClassNodeStack.Current().(*ir.ClassStmt)
-	if !ok || class.Extends == nil {
-		return
-	}
-	m, ok := solver.FindMethod(d.metaInfo(), d.ctx.st.CurrentParentClass, `__construct`)
-	if !ok || m.Info.AccessLevel == meta.Private || m.Info.IsAbstract() {
-		return
-	}
+type parseFuncParamsResult struct {
+	params         []meta.FuncParam
+	paramsTypeHint map[string]types.Map
+	minParamsCount int
+}
 
-	if !parentConstructorCalled {
-		d.Report(n, LevelWarning, "parentConstructor", "Missing parent::__construct() call")
+func (d *rootWalker) parseFuncParams(params []ir.Node, phpDocParamsTypes phpdoctypes.ParamsMap, sc *meta.Scope, closureSolver *solver.ClosureCallerInfo) (res parseFuncParamsResult) {
+	if len(params) == 0 {
+		return res
 	}
-}
 
-func (d *rootWalker) getElementPos(n ir.Node) meta.ElementPosition {
-	pos := ir.GetPosition(n)
-	_, startChar := d.parseStartPos(pos)
+	minArgs := 0
+	args := make([]meta.FuncParam, 0, len(params))
+	typeHints := make(map[string]types.Map, len(params))
 
-	return meta.ElementPosition{
-		Filename:  d.ctx.st.CurrentFile,
-		Character: int32(startChar),
-		Line:      int32(pos.StartLine),
-		EndLine:   int32(pos.EndLine),
-		Length:    int32(pos.EndPos - pos.StartPos),
+	if closureSolver != nil && solver.IsSupportedFunction(closureSolver.Name) {
+		return d.parseFuncArgsForCallback(params, sc, closureSolver)
 	}
-}
 
-type methodModifiers struct {
-	abstract       bool
-	static         bool
-	accessLevel    meta.AccessLevel
-	final          bool
-	accessImplicit bool
-}
+	for _, param := range params {
+		p := param.(*ir.Parameter)
+		v := p.Variable
+		phpDocType := phpDocParamsTypes[v.Name]
 
-func (d *rootWalker) parseMethodModifiers(meth *ir.ClassMethodStmt) (res methodModifiers) {
-	res.accessLevel = meta.Public
-	res.accessImplicit = true
+		if !phpDocType.Typ.Empty() {
+			sc.AddVarName(v.Name, phpDocType.Typ, "param", meta.VarAlwaysDefined)
+		}
 
-	for _, m := range meth.Modifiers {
-		switch d.lowerCaseModifier(m) {
-		case "abstract":
-			res.abstract = true
-		case "static":
-			res.static = true
-		case "public":
-			res.accessLevel = meta.Public
-			res.accessImplicit = false
-		case "private":
-			res.accessLevel = meta.Private
-			res.accessImplicit = false
-		case "protected":
-			res.accessLevel = meta.Protected
-			res.accessImplicit = false
-		case "final":
-			res.final = true
-		default:
-			linterError(d.ctx.st.CurrentFile, "Unrecognized method modifier: %s", m.Value)
+		paramTyp := phpDocType.Typ
+
+		if p.DefaultValue == nil && !phpDocType.Optional && !p.Variadic {
+			minArgs++
 		}
-	}
 
-	return res
-}
+		if p.VariableType != nil {
+			typeHintType, ok := d.parseTypeHintNode(p.VariableType)
+			if ok {
+				paramTyp = typeHintType
+			}
 
-func (d *rootWalker) checkMagicMethod(meth ir.Node, name string, modif methodModifiers, countArgs int) {
-	const Any = -1
-	var (
-		canBeStatic    bool
-		canBeNonPublic bool
-		mustBeStatic   bool
-
-		numArgsExpected int
-	)
-
-	switch name {
-	case "__call",
-		"__set":
-		canBeStatic = false
-		canBeNonPublic = false
-		numArgsExpected = 2
-
-	case "__get",
-		"__isset",
-		"__unset":
-		canBeStatic = false
-		canBeNonPublic = false
-		numArgsExpected = 1
-
-	case "__toString":
-		canBeStatic = false
-		canBeNonPublic = false
-		numArgsExpected = 0
-
-	case "__invoke",
-		"__debugInfo":
-		canBeStatic = false
-		canBeNonPublic = false
-		numArgsExpected = Any
-
-	case "__construct":
-		canBeStatic = false
-		canBeNonPublic = true
-		numArgsExpected = Any
-
-	case "__destruct", "__clone":
-		canBeStatic = false
-		canBeNonPublic = true
-		numArgsExpected = 0
-
-	case "__callStatic":
-		canBeStatic = true
-		canBeNonPublic = false
-		mustBeStatic = true
-		numArgsExpected = 2
-
-	case "__sleep",
-		"__wakeup",
-		"__serialize",
-		"__unserialize",
-		"__set_state":
-		canBeNonPublic = true
-		canBeStatic = true
-		numArgsExpected = Any
+			typeHints[v.Name] = typeHintType
+		} else if paramTyp.Empty() && p.DefaultValue != nil {
+			paramTyp = solver.ExprTypeLocal(sc, d.ctx.st, p.DefaultValue)
+			// For the type resolver default value can look like a
+			// precise source of information (e.g. "false" is a precise bool),
+			// but it's not assigned unconditionally.
+			// If explicit argument is provided, that parameter can have
+			// almost any type possible.
+			paramTyp.MarkAsImprecise()
+		}
 
-	default:
-		return // Not a magic method
-	}
+		if p.Variadic {
+			paramTyp = paramTyp.Map(types.WrapArrayOf)
+		}
 
-	switch {
-	case mustBeStatic && !modif.static:
-		d.Report(meth, LevelError, "magicMethodDecl", "The magic method %s() must be static", name)
-	case !canBeStatic && modif.static:
-		d.Report(meth, LevelError, "magicMethodDecl", "The magic method %s() cannot be static", name)
-	}
-	if !canBeNonPublic && modif.accessLevel != meta.Public {
-		d.Report(meth, LevelError, "magicMethodDecl", "The magic method %s() must have public visibility", name)
+		sc.AddVarName(v.Name, paramTyp, "param", meta.VarAlwaysDefined)
+
+		par := meta.FuncParam{
+			Typ:   paramTyp.Immutable(),
+			IsRef: p.ByRef,
+		}
+
+		par.Name = v.Name
+		args = append(args, par)
 	}
 
-	if countArgs != numArgsExpected && numArgsExpected != Any {
-		d.Report(meth, LevelError, "magicMethodDecl", "The magic method %s() must take exactly %d argument", name, numArgsExpected)
+	return parseFuncParamsResult{
+		params:         args,
+		paramsTypeHint: typeHints,
+		minParamsCount: minArgs,
 	}
 }
 
-func (d *rootWalker) getClass() meta.ClassInfo {
-	var m meta.ClassesMap
+// callbackParamByIndex returns the description of the parameter for the function by its index.
+func (d *rootWalker) callbackParamByIndex(param ir.Node, argType types.Map) meta.FuncParam {
+	p := param.(*ir.Parameter)
+	v := p.Variable
 
-	if d.ctx.st.IsTrait {
-		if d.meta.Traits.H == nil {
-			d.meta.Traits = meta.NewClassesMap()
-		}
-		m = d.meta.Traits
+	var typ types.Map
+	tp, ok := d.parseTypeHintNode(p.VariableType)
+	if ok {
+		typ = tp
 	} else {
-		if d.meta.Classes.H == nil {
-			d.meta.Classes = meta.NewClassesMap()
-		}
-		m = d.meta.Classes
+		typ = argType.Map(types.WrapElemOf)
 	}
 
-	cl, ok := m.Get(d.ctx.st.CurrentClass)
-	if !ok {
-		var flags meta.ClassFlags
-		if d.ctx.st.IsInterface {
-			flags = meta.ClassInterface
-		}
-
-		cl = meta.ClassInfo{
-			Pos:              d.getElementPos(d.currentClassNodeStack.Current()),
-			Name:             d.ctx.st.CurrentClass,
-			Flags:            flags,
-			Parent:           d.ctx.st.CurrentParentClass,
-			ParentInterfaces: d.ctx.st.CurrentParentInterfaces,
-			Interfaces:       make(map[string]struct{}),
-			Traits:           make(map[string]struct{}),
-			Methods:          meta.NewFunctionsMap(),
-			Properties:       make(meta.PropertiesMap),
-			Constants:        make(meta.ConstantsMap),
-		}
-
-		m.Set(d.ctx.st.CurrentClass, cl)
+	arg := meta.FuncParam{
+		IsRef: p.ByRef,
+		Name:  v.Name,
+		Typ:   typ,
 	}
-
-	return cl
+	return arg
 }
 
-func (d *rootWalker) lowerCaseModifier(m *ir.Identifier) string {
-	lcase := strings.ToLower(m.Value)
-	if lcase != m.Value {
-		d.Report(m, LevelWarning, "keywordCase", "Use %s instead of %s",
-			lcase, m.Value)
+func (d *rootWalker) parseFuncArgsForCallback(params []ir.Node, sc *meta.Scope, closureSolver *solver.ClosureCallerInfo) (res parseFuncParamsResult) {
+	countParams := len(params)
+	minArgs := countParams
+	if countParams == 0 {
+		return res
 	}
-	return lcase
-}
-
-func (d *rootWalker) enterPropertyList(pl *ir.PropertyListStmt) bool {
-	cl := d.getClass()
-
-	isStatic := false
-	accessLevel := meta.Public
-	accessImplicit := true
+	args := make([]meta.FuncParam, countParams)
 
-	for _, m := range pl.Modifiers {
-		switch d.lowerCaseModifier(m) {
-		case "public":
-			accessLevel = meta.Public
-			accessImplicit = false
-		case "protected":
-			accessLevel = meta.Protected
-			accessImplicit = false
-		case "private":
-			accessLevel = meta.Private
-			accessImplicit = false
-		case "static":
-			isStatic = true
+	switch closureSolver.Name {
+	case `\usort`, `\uasort`, `\array_reduce`:
+		args[0] = d.callbackParamByIndex(params[0], closureSolver.ArgTypes[0])
+		if countParams > 1 {
+			args[1] = d.callbackParamByIndex(params[1], closureSolver.ArgTypes[0])
 		}
+	case `\array_walk`, `\array_walk_recursive`, `\array_filter`:
+		args[0] = d.callbackParamByIndex(params[0], closureSolver.ArgTypes[0])
+	case `\array_map`:
+		args[0] = d.callbackParamByIndex(params[0], closureSolver.ArgTypes[1])
 	}
 
-	if accessImplicit {
-		target := "property"
-		if len(pl.Properties) > 1 {
-			target = "properties"
+	for i, param := range params {
+		p := param.(*ir.Parameter)
+		v := p.Variable
+		var typ types.Map
+		if i < len(args) {
+			typ = args[i].Typ
+		} else {
+			typ = types.MixedType
 		}
-		d.Report(pl, LevelNotice, "implicitModifiers", "Specify the access modifier for %s explicitly", target)
+
+		sc.AddVarName(v.Name, typ, "param", meta.VarAlwaysDefined)
+	}
+
+	return parseFuncParamsResult{
+		params:         args,
+		minParamsCount: minArgs,
 	}
+}
 
-	d.checkCommentMisspellings(pl, pl.Doc.Raw)
-	phpDocType := d.parsePHPDocVar(pl, pl.Doc)
+func (d *rootWalker) enterFunction(fun *ir.FunctionStmt) bool {
+	nm := d.ctx.st.Namespace + `\` + fun.FunctionName.Value
+	pos := ir.GetPosition(fun)
 
-	typeHintType, ok := d.parseTypeHintNode(pl.Type)
-	if ok && !d.typeHintHasMoreAccurateType(typeHintType, phpDocType) {
-		d.Report(pl, LevelNotice, "typeHint", "Specify the type for the property in PHPDoc, 'array' type hint too generic")
+	if funcSize := pos.EndLine - pos.StartLine; funcSize > maxFunctionLines {
+		d.Report(fun.FunctionName, LevelNotice, "complexity", "Too big function: more than %d lines", maxFunctionLines)
 	}
-	d.checkTypeHintNode(pl.Type, "property type")
 
-	for _, pNode := range pl.Properties {
-		prop := pNode.(*ir.PropertyStmt)
+	if d.meta.Functions.H == nil {
+		d.meta.Functions = meta.NewFunctionsMap()
+	}
 
-		nm := prop.Variable.Name
+	d.checker.CheckCommentMisspellings(fun.FunctionName, fun.Doc.Raw)
+	d.checker.CheckIdentMisspellings(fun.FunctionName)
 
-		// We need to clone the types, because otherwise, if several
-		// properties are written in one definition, and null was
-		// assigned to the first, then all properties become nullable.
-		propTypes := phpDocType.Clone().Append(typeHintType)
+	// Indexing stage.
+	doc := phpdoctypes.Parse(fun.Doc, fun.Params, d.ctx.typeNormalizer)
+	moveShapesToContext(&d.ctx, doc.Shapes)
+	d.handleClosuresFromDoc(doc.Closures)
 
-		d.checkAssignNullToNotNullableProperty(prop, propTypes)
+	// Check stage.
+	errors := d.checker.CheckPHPDoc(fun, fun.Doc, fun.Params)
+	d.reportPHPDocErrors(errors)
 
-		if prop.Expr != nil {
-			propTypes = propTypes.Append(solver.ExprTypeLocal(d.scope(), d.ctx.st, prop.Expr))
-		}
+	phpDocReturnType := doc.ReturnType
+	phpDocParamTypes := doc.ParamTypes
 
-		if isStatic {
-			nm = "$" + nm
-		}
+	sc := meta.NewScope()
 
-		// TODO: handle duplicate property
-		cl.Properties[nm] = meta.PropertyInfo{
-			Pos:         d.getElementPos(prop),
-			Typ:         propTypes.Immutable(),
-			AccessLevel: accessLevel,
-		}
+	returnTypeHint, ok := d.parseTypeHintNode(fun.ReturnType)
+	if ok && !doc.Inherit {
+		d.checker.CheckFuncReturnType(fun.FunctionName, fun.FunctionName.Value, returnTypeHint, phpDocReturnType)
 	}
+	d.checker.CheckTypeHintNode(fun.ReturnType, "return type")
 
-	return true
-}
+	funcParams := d.parseFuncParams(fun.Params, phpDocParamTypes, sc, nil)
 
-func (d *rootWalker) checkAssignNullToNotNullableProperty(prop *ir.PropertyStmt, propTypes types.Map) {
-	assignNull := false
+	d.checker.CheckFuncParams(fun.FunctionName, fun.Params, funcParams, phpDocParamTypes)
 
-	if expr, ok := prop.Expr.(*ir.ConstFetchExpr); ok {
-		assignNull = strings.EqualFold(expr.Constant.Value, "null")
-	}
+	funcInfo := d.handleFuncStmts(funcParams.params, nil, fun.Stmts, sc)
+	actualReturnTypes := funcInfo.returnTypes
+	exitFlags := funcInfo.prematureExitFlags
 
-	if assignNull && !propTypes.Empty() {
-		onlyClasses := true
-		nullable := propTypes.Find(func(typ string) bool {
-			if !types.IsClass(typ) && typ != "null" {
-				onlyClasses = false
-			}
-			return typ == "null"
-		})
+	returnTypes := functionReturnType(phpDocReturnType, returnTypeHint, actualReturnTypes)
 
-		if !nullable && onlyClasses {
-			d.Report(prop, LevelNotice, "propNullDefault", "Assigning null to a not nullable property may soon cause a KPHP compilation error")
-			d.addFixForNullForNotNullableProperty(prop)
-		}
+	var funcFlags meta.FuncFlags
+	if solver.SideEffectFreeFunc(d.scope(), d.ctx.st, nil, fun.Stmts) {
+		funcFlags |= meta.FuncPure
 	}
+	d.meta.Functions.Set(nm, meta.FuncInfo{
+		Params:       funcParams.params,
+		Name:         nm,
+		Pos:          d.getElementPos(fun),
+		Typ:          returnTypes.Immutable(),
+		MinParamsCnt: funcParams.minParamsCount,
+		Flags:        funcFlags,
+		ExitFlags:    exitFlags,
+		Doc:          doc.AdditionalInfo,
+	})
+
+	return false
 }
 
-func (d *rootWalker) addFixForNullForNotNullableProperty(prop *ir.PropertyStmt) {
-	if !d.config.ApplyQuickFixes {
-		return
-	}
+// Handle functions call part.
 
-	from := prop.Position.StartPos
-	to := prop.Variable.Position.EndPos
+func (d *rootWalker) enterFunctionCall(s *ir.FunctionCallExpr) bool {
+	nm, ok := s.Function.(*ir.Name)
+	if !ok {
+		return true
+	}
 
-	withoutAssign := d.file.Contents()[from:to]
+	name := strings.TrimPrefix(nm.Value, `\`)
 
-	d.addQuickFix("propNullDefault", quickfix.TextEdit{
-		StartPos:    prop.Position.StartPos,
-		EndPos:      prop.Position.EndPos,
-		Replacement: string(withoutAssign),
-	})
+	if d.ctx.st.Namespace == `\PHPSTORM_META` && name == `override` {
+		return d.handleOverride(s)
+	}
+
+	if name == "define" {
+		d.handleDefineCall(s)
+	}
+
+	return true
+}
+
+func (d *rootWalker) handleDefineCall(s *ir.FunctionCallExpr) {
+	if len(s.Args) < 2 {
+		return
+	}
+
+	arg := s.Arg(0)
+
+	str, ok := arg.Expr.(*ir.String)
+	if !ok {
+		return
+	}
+
+	valueArg := s.Arg(1)
+
+	if d.meta.Constants == nil {
+		d.meta.Constants = make(meta.ConstantsMap)
+	}
+
+	value := constfold.Eval(d.ctx.st, valueArg)
+
+	d.meta.Constants[`\`+strings.TrimFunc(str.Value, utils.IsQuote)] = meta.ConstInfo{
+		Pos:   d.getElementPos(s),
+		Typ:   solver.ExprTypeLocal(d.scope(), d.ctx.st, valueArg.Expr),
+		Value: value,
+	}
+}
+
+// handleOverride handle e.g. "override(\array_shift(0), elementType(0));"
+// which means "return type of array_shift() is the type of element of first function parameter"
+func (d *rootWalker) handleOverride(s *ir.FunctionCallExpr) bool {
+	if len(s.Args) != 2 {
+		return true
+	}
+
+	arg0 := s.Arg(0)
+	arg1 := s.Arg(1)
+
+	fc0, ok := arg0.Expr.(*ir.FunctionCallExpr)
+	if !ok {
+		return true
+	}
+
+	fc1, ok := arg1.Expr.(*ir.FunctionCallExpr)
+	if !ok {
+		return true
+	}
+
+	fnNameNode, ok := fc0.Function.(*ir.Name)
+	if !ok || !fnNameNode.IsFullyQualified() {
+		return true
+	}
+
+	overrideNameNode, ok := fc1.Function.(*ir.Name)
+	if !ok {
+		return true
+	}
+
+	if len(fc1.Args) != 1 {
+		return true
+	}
+
+	fc1Arg0 := fc1.Arg(0)
+
+	argNumNode, ok := fc1Arg0.Expr.(*ir.Lnumber)
+	if !ok {
+		return true
+	}
+
+	argNum, err := strconv.Atoi(argNumNode.Value)
+	if err != nil {
+		return true
+	}
+
+	var overrideTyp meta.OverrideType
+	switch {
+	case overrideNameNode.Value == `type`:
+		overrideTyp = meta.OverrideArgType
+	case overrideNameNode.Value == `elementType`:
+		overrideTyp = meta.OverrideElementType
+	default:
+		return true
+	}
+
+	fnName := fnNameNode.Value
+
+	if d.meta.FunctionOverrides == nil {
+		d.meta.FunctionOverrides = make(meta.FunctionsOverrideMap)
+	}
+
+	d.meta.FunctionOverrides[fnName] = meta.FuncInfoOverride{
+		OverrideType: overrideTyp,
+		ArgNum:       argNum,
+	}
+
+	return true
+}
+
+// Handle class part.
+
+func (d *rootWalker) getClassModifiers(n *ir.ClassStmt) meta.ClassFlags {
+	var classFlags meta.ClassFlags
+	for _, m := range n.Modifiers {
+		switch {
+		case strings.EqualFold("abstract", m.Value):
+			classFlags |= meta.ClassAbstract
+		case strings.EqualFold("final", m.Value):
+			classFlags |= meta.ClassFinal
+		}
+	}
+	return classFlags
+}
+
+func (d *rootWalker) parseClassPHPDoc(class ir.Node, doc phpdoc.Comment) classPHPDocParseResult {
+	var result classPHPDocParseResult
+
+	if doc.Raw == "" {
+		return result
+	}
+
+	// TODO: allocate maps lazily.
+	// Class may not have any @property or @method annotations.
+	// In that case we can handle avoid map allocations.
+	result.properties = make(meta.PropertiesMap)
+	result.methods = meta.NewFunctionsMap()
+
+	for _, part := range doc.Parsed {
+		d.checker.checkPHPDocRef(class, part)
+		switch part.Name() {
+		case "property", "property-read", "property-write":
+			parseClassPHPDocProperty(class, &d.ctx, &result, part.(*phpdoc.TypeVarCommentPart))
+		case "method":
+			parseClassPHPDocMethod(class, &d.ctx, &result, part.(*phpdoc.RawCommentPart))
+		case "mixin":
+			parseClassPHPDocMixin(class, d.ctx.st, &result, part.(*phpdoc.RawCommentPart))
+		}
+	}
+
+	return result
+}
+
+func (d *rootWalker) handleClassDoc(doc classPHPDocParseResult, cl *meta.ClassInfo) {
+	// If we ever need to distinguish @property-annotated and real properties,
+	// more work will be required here.
+	for name, p := range doc.properties {
+		p.Pos = cl.Pos
+		cl.Properties[name] = p
+	}
+
+	for name, m := range doc.methods.H {
+		m.Pos = cl.Pos
+		cl.Methods.H[name] = m
+	}
+
+	cl.Mixins = doc.mixins
+}
+
+func (d *rootWalker) parsePHPDocVar(doc phpdoc.Comment) (typesMap types.Map) {
+	for _, part := range doc.Parsed {
+		part, ok := part.(*phpdoc.TypeVarCommentPart)
+		if ok && part.Name() == "var" {
+			converted := phpdoctypes.ToRealType(d.ctx.typeNormalizer.ClassFQNProvider(), part.Type)
+			moveShapesToContext(&d.ctx, converted.Shapes)
+			d.handleClosuresFromDoc(converted.Closures)
+
+			typesMap = types.NewMapWithNormalization(d.ctx.typeNormalizer, converted.Types)
+		}
+	}
+
+	return typesMap
+}
+
+func (d *rootWalker) enterPropertyList(pl *ir.PropertyListStmt) bool {
+	cl := d.getClass()
+
+	isStatic := false
+	accessLevel := meta.Public
+	accessImplicit := true
+
+	for _, m := range pl.Modifiers {
+		d.checker.CheckModifierKeywordCase(m)
+		switch strings.ToLower(m.Value) {
+		case "public":
+			accessLevel = meta.Public
+			accessImplicit = false
+		case "protected":
+			accessLevel = meta.Protected
+			accessImplicit = false
+		case "private":
+			accessLevel = meta.Private
+			accessImplicit = false
+		case "static":
+			isStatic = true
+		}
+	}
+
+	if accessImplicit {
+		target := "property"
+		if len(pl.Properties) > 1 {
+			target = "properties"
+		}
+		d.Report(pl, LevelNotice, "implicitModifiers", "Specify the access modifier for %s explicitly", target)
+	}
+
+	d.checker.CheckCommentMisspellings(pl, pl.Doc.Raw)
+	phpDocType := d.parsePHPDocVar(pl.Doc)
+	d.checker.CheckPHPDocVar(pl, pl.Doc, phpDocType)
+
+	typeHintType, ok := d.parseTypeHintNode(pl.Type)
+	if ok && !types.TypeHintHasMoreAccurateType(typeHintType, phpDocType) {
+		d.Report(pl, LevelNotice, "typeHint", "Specify the type for the property in PHPDoc, 'array' type hint too generic")
+	}
+	d.checker.CheckTypeHintNode(pl.Type, "property type")
+
+	for _, pNode := range pl.Properties {
+		prop := pNode.(*ir.PropertyStmt)
+
+		nm := prop.Variable.Name
+
+		// We need to clone the types, because otherwise, if several
+		// properties are written in one definition, and null was
+		// assigned to the first, then all properties become nullable.
+		propTypes := phpDocType.Clone().Append(typeHintType)
+
+		d.checker.CheckAssignNullToNotNullableProperty(prop, propTypes)
+
+		if prop.Expr != nil {
+			propTypes = propTypes.Append(solver.ExprTypeLocal(d.scope(), d.ctx.st, prop.Expr))
+		}
+
+		if isStatic {
+			nm = "$" + nm
+		}
+
+		// TODO: handle duplicate property
+		cl.Properties[nm] = meta.PropertyInfo{
+			Pos:         d.getElementPos(prop),
+			Typ:         propTypes.Immutable(),
+			AccessLevel: accessLevel,
+		}
+	}
+
+	return true
 }
 
 func (d *rootWalker) enterClassConstList(list *ir.ClassConstListStmt) bool {
@@ -1062,7 +932,8 @@ func (d *rootWalker) enterClassConstList(list *ir.ClassConstListStmt) bool {
 	accessLevel := meta.Public
 
 	for _, m := range list.Modifiers {
-		switch d.lowerCaseModifier(m) {
+		d.checker.CheckModifierKeywordCase(m)
+		switch strings.ToLower(m.Value) {
 		case "public":
 			accessLevel = meta.Public
 		case "protected":
@@ -1076,7 +947,7 @@ func (d *rootWalker) enterClassConstList(list *ir.ClassConstListStmt) bool {
 		c := cNode.(*ir.ConstantStmt)
 
 		nm := c.ConstantName.Value
-		d.checkCommentMisspellings(c, list.Doc.Raw)
+		d.checker.CheckCommentMisspellings(c, list.Doc.Raw)
 		typ := solver.ExprTypeLocal(d.scope(), d.ctx.st, c.Expr)
 
 		value := constfold.Eval(d.ctx.st, c.Expr)
@@ -1093,28 +964,11 @@ func (d *rootWalker) enterClassConstList(list *ir.ClassConstListStmt) bool {
 	return true
 }
 
-func (d *rootWalker) checkOldStyleConstructor(meth *ir.ClassMethodStmt) {
-	lastDelim := strings.LastIndexByte(d.ctx.st.CurrentClass, '\\')
-	methodName := meth.MethodName.Value
-	className := d.ctx.st.CurrentClass[lastDelim+1:]
-
-	if !strings.EqualFold(className, methodName) {
-		return
-	}
-
-	_, inClass := d.currentClassNodeStack.Current().(*ir.ClassStmt)
-	if !inClass {
-		return
-	}
-
-	d.Report(meth.MethodName, LevelNotice, "oldStyleConstructor", "Old-style constructor usage, use __construct instead")
-}
-
 func (d *rootWalker) enterClassMethod(meth *ir.ClassMethodStmt) bool {
 	nm := meth.MethodName.Value
 	_, insideInterface := d.currentClassNodeStack.Current().(*ir.InterfaceStmt)
 
-	d.checkOldStyleConstructor(meth)
+	d.checker.CheckOldStyleConstructor(meth)
 
 	pos := ir.GetPosition(meth)
 
@@ -1131,7 +985,7 @@ func (d *rootWalker) enterClassMethod(meth *ir.ClassMethodStmt) bool {
 		d.Report(meth.MethodName, LevelNotice, "implicitModifiers", "Specify the access modifier for %s method explicitly", methodFQN)
 	}
 
-	d.checkMagicMethod(meth.MethodName, nm, modif, len(meth.Params))
+	d.checker.CheckMagicMethod(meth.MethodName, nm, modif, len(meth.Params))
 
 	sc := meta.NewScope()
 	if !modif.static {
@@ -1146,8 +1000,8 @@ func (d *rootWalker) enterClassMethod(meth *ir.ClassMethodStmt) bool {
 			d.Report(meth.MethodName, LevelNotice, "missingPhpdoc", "Missing PHPDoc for %s public method", methodFQN)
 		}
 	}
-	d.checkCommentMisspellings(meth.MethodName, meth.Doc.Raw)
-	d.checkIdentMisspellings(meth.MethodName)
+	d.checker.CheckCommentMisspellings(meth.MethodName, meth.Doc.Raw)
+	d.checker.CheckIdentMisspellings(meth.MethodName)
 
 	// Indexing stage.
 	doc := phpdoctypes.Parse(meth.Doc, meth.Params, d.ctx.typeNormalizer)
@@ -1155,7 +1009,7 @@ func (d *rootWalker) enterClassMethod(meth *ir.ClassMethodStmt) bool {
 	d.handleClosuresFromDoc(doc.Closures)
 
 	// Check stage.
-	errors := d.checkPHPDoc(meth, meth.Doc, meth.Params)
+	errors := d.checker.CheckPHPDoc(meth, meth.Doc, meth.Params)
 	d.reportPHPDocErrors(errors)
 
 	phpDocReturnType := doc.ReturnType
@@ -1163,13 +1017,13 @@ func (d *rootWalker) enterClassMethod(meth *ir.ClassMethodStmt) bool {
 
 	returnTypeHint, ok := d.parseTypeHintNode(meth.ReturnType)
 	if ok && !doc.Inherit {
-		d.checkFuncReturnType(meth.MethodName, meth.MethodName.Value, returnTypeHint, phpDocReturnType)
+		d.checker.CheckFuncReturnType(meth.MethodName, meth.MethodName.Value, returnTypeHint, phpDocReturnType)
 	}
-	d.checkTypeHintNode(meth.ReturnType, "return type")
+	d.checker.CheckTypeHintNode(meth.ReturnType, "return type")
 
 	funcParams := d.parseFuncParams(meth.Params, phpDocParamTypes, sc, nil)
 
-	d.checkFuncParams(meth.MethodName, meth.Params, funcParams, phpDocParamTypes)
+	d.checker.CheckFuncParams(meth.MethodName, meth.Params, funcParams, phpDocParamTypes)
 
 	if len(class.Interfaces) != 0 {
 		// If we implement interfaces, methods that take a part in this
@@ -1213,7 +1067,7 @@ func (d *rootWalker) enterClassMethod(meth *ir.ClassMethodStmt) bool {
 	actualReturnTypes := funcInfo.returnTypes
 	exitFlags := funcInfo.prematureExitFlags
 	if nm == `__construct` {
-		d.checkParentConstructorCall(meth.MethodName, funcInfo.callsParentConstructor)
+		d.checker.CheckParentConstructorCall(meth.MethodName, funcInfo.callsParentConstructor)
 	}
 
 	returnTypes := functionReturnType(phpDocReturnType, returnTypeHint, actualReturnTypes)
@@ -1257,962 +1111,522 @@ func (d *rootWalker) enterClassMethod(meth *ir.ClassMethodStmt) bool {
 	return false
 }
 
-func (d *rootWalker) reportPHPDocErrors(errs PHPDocErrors) {
-	for _, err := range errs.types {
-		d.ReportPHPDoc(err.Location, LevelNotice, "phpdocType", err.Message)
-	}
-	for _, err := range errs.lint {
-		d.ReportPHPDoc(err.Location, LevelWarning, "phpdocLint", err.Message)
-	}
+type methodModifiers struct {
+	abstract       bool
+	static         bool
+	accessLevel    meta.AccessLevel
+	final          bool
+	accessImplicit bool
 }
 
-func (d *rootWalker) parsePHPDocVar(n ir.Node, doc phpdoc.Comment) (typesMap types.Map) {
-	if phpdoc.IsSuspicious([]byte(doc.Raw)) {
-		d.ReportPHPDoc(PHPDocLine(n, 1),
-			LevelWarning, "phpdocLint",
-			"Multiline PHPDoc comment should start with /**, not /*",
-		)
-	}
-
-	for _, part := range doc.Parsed {
-		d.checkPHPDocRef(n, part)
-		part, ok := part.(*phpdoc.TypeVarCommentPart)
-		if ok && part.Name() == "var" {
-			converted := phpdoctypes.ToRealType(d.ctx.typeNormalizer.ClassFQNProvider(), part.Type)
-			moveShapesToContext(&d.ctx, converted.Shapes)
-			d.handleClosuresFromDoc(converted.Closures)
-
-			if converted.Warning != "" {
-				field := 1
-				if part.VarIsFirst {
-					field = 2
-				}
-				d.ReportPHPDoc(PHPDocLineField(n, part.Line(), field),
-					LevelNotice, "phpdocType",
-					converted.Warning,
-				)
-			}
-
-			typesMap = types.NewMapWithNormalization(d.ctx.typeNormalizer, converted.Types)
+func (d *rootWalker) parseMethodModifiers(meth *ir.ClassMethodStmt) (res methodModifiers) {
+	res.accessLevel = meta.Public
+	res.accessImplicit = true
 
-			d.checkUndefinedClassesInPHPDoc(n, typesMap, part)
+	for _, m := range meth.Modifiers {
+		d.checker.CheckModifierKeywordCase(m)
+		switch strings.ToLower(m.Value) {
+		case "abstract":
+			res.abstract = true
+		case "static":
+			res.static = true
+		case "public":
+			res.accessLevel = meta.Public
+			res.accessImplicit = false
+		case "private":
+			res.accessLevel = meta.Private
+			res.accessImplicit = false
+		case "protected":
+			res.accessLevel = meta.Protected
+			res.accessImplicit = false
+		case "final":
+			res.final = true
+		default:
+			linterError(d.ctx.st.CurrentFile, "Unrecognized method modifier: %s", m.Value)
 		}
 	}
 
-	return typesMap
+	return res
 }
 
-func (d *rootWalker) checkUndefinedClassesInPHPDoc(n ir.Node, typesMap types.Map, part phpdoc.CommentPart) {
-	if !d.metaInfo().IsIndexingComplete() {
-		return
+// Handle const list part.
+
+func (d *rootWalker) enterConstList(lst *ir.ConstListStmt) bool {
+	if d.meta.Constants == nil {
+		d.meta.Constants = make(meta.ConstantsMap)
 	}
 
-	resolved := solver.ResolveTypes(d.metaInfo(), d.ctx.st.CurrentClass, typesMap, solver.ResolverMap{})
-	typesMap = types.NewMapFromMap(resolved)
+	for _, sNode := range lst.Consts {
+		s := sNode.(*ir.ConstantStmt)
 
-	typesMap.Iterate(func(className string) {
-		if types.IsShape(className) {
-			shape, ok := d.metaInfo().GetClass(className)
-			if ok {
-				for _, info := range shape.Properties {
-					info.Typ.Iterate(func(typ string) {
-						if !types.IsClass(typ) {
-							return
-						}
-
-						d.checkUndefinedClass(typ, part, n)
-					})
-				}
-			}
-			return
-		}
+		value := constfold.Eval(d.ctx.st, s.Expr)
 
-		if types.IsArray(className) {
-			arrayType := types.ArrayType(className)
-			if types.IsClass(arrayType) {
-				d.checkUndefinedClass(arrayType, part, n)
-			}
-			return
-		}
+		id := s.ConstantName
+		nm := d.ctx.st.Namespace + `\` + id.Value
 
-		if !types.IsClass(className) {
-			return
+		d.meta.Constants[nm] = meta.ConstInfo{
+			Pos:   d.getElementPos(s),
+			Typ:   solver.ExprTypeLocal(d.scope(), d.ctx.st, s.Expr),
+			Value: value,
 		}
-
-		d.checkUndefinedClass(className, part, n)
-	})
-}
-
-func (d *rootWalker) checkUndefinedClass(className string, part phpdoc.CommentPart, n ir.Node) {
-	// While there is no template support, this hack saves you unnecessary bugs.
-	if strings.HasSuffix(className, `\T`) {
-		return
-	}
-
-	_, ok := d.metaInfo().GetClassOrTrait(className)
-	if ok {
-		return
-	}
-	partNum := 1
-	if varPart, ok := part.(*phpdoc.TypeVarCommentPart); ok && varPart.VarIsFirst {
-		partNum = 2
 	}
 
-	d.ReportPHPDoc(PHPDocLineField(n, part.Line(), partNum),
-		LevelError, "undefinedClass",
-		"Class or interface named %s does not exist", className,
-	)
+	return false
 }
 
-func (d *rootWalker) isValidPHPDocRef(n ir.Node, ref string) bool {
-	// Skip:
-	// - URLs
-	// - Things that can be a filename (e.g. "foo.php")
-	// - Wildcards (e.g. "self::FOO*")
-	// - Issue references (e.g. "#1393" "BACK-103")
-	// - RFCs
-	if strings.Contains(ref, "http:") || strings.Contains(ref, "https:") {
-		return true // OK: URL?
-	}
-	if strings.Contains(ref, "RFC") {
-		return true
-	}
-	if strings.ContainsAny(ref, ".*-#") {
-		return true
+// Utils part.
+
+func (d *rootWalker) handleClosuresFromDoc(closures types.ClosureMap) {
+	if d.meta.Functions.H == nil {
+		d.meta.Functions = meta.NewFunctionsMap()
 	}
 
-	// expandName tries to convert s symbol into fully qualified form.
-	expandName := func(s string) string {
-		s, ok := solver.GetClassName(d.ctx.st, &ir.Name{Value: s})
-		if !ok {
-			return s
+	for name, closureInfo := range closures {
+		var params []meta.FuncParam
+		for i, paramType := range closureInfo.ParamTypes {
+			params = append(params, meta.FuncParam{
+				Name: fmt.Sprintf("closure param #%d", i),
+				Typ:  types.NewMapWithNormalization(d.ctx.typeNormalizer, paramType),
+			})
 		}
-		return s
-	}
 
-	isValidGlobalVar := func(ref string) bool {
-		// Since we don't have an exhaustive list of globals,
-		// we can't tell for sure whether a variable ref is correct.
-		return true
+		d.meta.Functions.Set(name, meta.FuncInfo{
+			Params:       params,
+			Name:         name,
+			Typ:          types.NewMapWithNormalization(d.ctx.typeNormalizer, closureInfo.ReturnType),
+			MinParamsCnt: len(closureInfo.ParamTypes),
+		})
 	}
+}
 
-	isValidClassSymbol := func(ref string) bool {
-		parts := strings.Split(ref, "::")
-		if len(parts) != 2 {
-			return false
-		}
-		typeName, symbolName := expandName(parts[0]), parts[1]
-		if symbolName == "class" {
-			_, ok := d.metaInfo().GetClass(typeName)
-			return ok
-		}
-		if strings.HasPrefix(symbolName, "$") {
-			return classHasProp(d.ctx.st, typeName, symbolName)
-		}
-		if _, ok := solver.FindMethod(d.metaInfo(), typeName, symbolName); ok {
-			return true
-		}
-		if _, _, ok := solver.FindConstant(d.metaInfo(), typeName, symbolName); ok {
-			return true
-		}
-		return false
+// parseTypeHintNode parse type info, e.g. "string" in "someFunc() : string { ... }".
+func (d *rootWalker) parseTypeHintNode(n ir.Node) (typ types.Map, ok bool) {
+	if n == nil {
+		return types.Map{}, false
 	}
 
-	isValidSymbol := func(ref string) bool {
-		if !strings.HasPrefix(ref, `\`) {
-			if d.currentClassNodeStack.Current() != nil {
-				className := d.ctx.st.CurrentClass
-				if _, ok := solver.FindMethod(d.metaInfo(), className, ref); ok {
-					return true // OK: class method reference
-				}
-				if _, _, ok := solver.FindConstant(d.metaInfo(), className, ref); ok {
-					return true // OK: class constant reference
-				}
-				if classHasProp(d.ctx.st, className, ref) {
-					return true // OK: class prop reference
-				}
-			}
+	typesMap := types.NormalizedTypeHintTypes(d.ctx.typeNormalizer, n)
 
-			// Functions and constants fall back in global namespace resolving.
-			// See https://www.php.net/manual/en/language.namespaces.fallback.php
-			globalRef := `\` + ref
-			if _, ok := d.metaInfo().GetFunction(globalRef); ok {
-				return true // OK: function reference
-			}
-			if _, ok := d.metaInfo().GetConstant(globalRef); ok {
-				return true // OK: const reference
-			}
-		}
-		fqnRef := expandName(ref)
-		if _, ok := d.metaInfo().GetFunction(fqnRef); ok {
-			return true // OK: FQN function reference
-		}
-		if _, ok := d.metaInfo().GetClass(fqnRef); ok {
-			return true // OK: FQN class reference
-		}
-		if _, ok := d.metaInfo().GetConstant(fqnRef); ok {
-			return true // OK: FQN const reference
-		}
-		return false
-	}
+	return typesMap, !typesMap.Empty()
+}
 
-	switch {
-	case strings.Contains(ref, "::"):
-		return isValidClassSymbol(ref)
-	case strings.HasPrefix(ref, "$"):
-		return isValidGlobalVar(ref)
-	default:
-		return isValidSymbol(ref)
+func (d *rootWalker) reportPHPDocErrors(errs PHPDocErrors) {
+	for _, err := range errs.types {
+		d.ReportPHPDoc(err.Location, LevelNotice, "phpdocType", err.Message)
+	}
+	for _, err := range errs.lint {
+		d.ReportPHPDoc(err.Location, LevelWarning, "phpdocLint", err.Message)
 	}
 }
 
-func (d *rootWalker) checkPHPDocRef(n ir.Node, part phpdoc.CommentPart) {
-	if !d.metaInfo().IsIndexingComplete() {
+func (d *rootWalker) addQuickFix(checkName string, fix quickfix.TextEdit) {
+	if !d.config.ApplyQuickFixes {
 		return
 	}
 
-	switch part.Name() {
-	case "mixin":
-		d.checkPHPDocMixinRef(n, part)
-	case "see":
-		d.checkPHPDocSeeRef(n, part)
+	if !d.checkersFilter.IsEnabledReport(checkName, d.ctx.st.CurrentFile) {
+		return
 	}
+
+	d.ctx.fixes = append(d.ctx.fixes, fix)
 }
 
-func (d *rootWalker) checkPHPDocSeeRef(n ir.Node, part phpdoc.CommentPart) {
-	params := part.(*phpdoc.RawCommentPart).Params
-	if len(params) == 0 {
-		return
+func (d *rootWalker) currentFunction() (meta.FuncInfo, bool) {
+	name := d.ctx.st.CurrentFunction
+	if name == "" {
+		return meta.FuncInfo{}, false
 	}
 
-	// @see supports a comma-separated list of refs.
-	var refs []string
-	for _, p := range params {
-		refs = append(refs, strings.TrimSuffix(p, ","))
-		if !strings.HasSuffix(p, ",") {
-			break
+	if d.ctx.st.CurrentClass != "" {
+		className, ok := solver.GetClassName(d.ctx.st, &ir.Name{Value: d.ctx.st.CurrentClass})
+		if !ok {
+			return meta.FuncInfo{}, false
 		}
-	}
 
-	for _, ref := range refs {
-		// Sometimes people write references like `foo()` `foo...` `foo@`.
-		ref = strings.TrimRight(ref, "().;@")
-		if !d.isValidPHPDocRef(n, ref) {
-			d.ReportPHPDoc(
-				PHPDocLineField(n, part.Line(), 1),
-				LevelWarning, "phpdocRef", "@see tag refers to unknown symbol %s", ref,
-			)
+		method, ok := solver.FindMethod(d.ctx.st.Info, className, name)
+		if !ok {
+			return meta.FuncInfo{}, false
 		}
-	}
-}
 
-func (d *rootWalker) checkPHPDocMixinRef(n ir.Node, part phpdoc.CommentPart) {
-	rawPart, ok := part.(*phpdoc.RawCommentPart)
-	if !ok {
-		return
+		return method.Info, true
 	}
 
-	params := rawPart.Params
-	if len(params) == 0 {
-		return
+	funcName, ok := solver.GetFuncName(d.ctx.st, &ir.Name{Value: name})
+	if !ok {
+		return meta.FuncInfo{}, false
 	}
 
-	name, ok := solver.GetClassName(d.ctx.st, &ir.Name{
-		Value: params[0],
-	})
-
+	fun, ok := d.ctx.st.Info.GetFunction(funcName)
 	if !ok {
-		return
+		return meta.FuncInfo{}, false
 	}
 
-	if _, ok := d.metaInfo().GetClass(name); !ok {
-		d.ReportPHPDoc(
-			PHPDocLineField(n, part.Line(), 1),
-			LevelWarning, "phpdocRef", "@mixin tag refers to unknown class %s", name,
-		)
-	}
+	return fun, true
 }
 
-func (d *rootWalker) checkPHPDoc(n ir.Node, doc phpdoc.Comment, actualParams []ir.Node) (errors PHPDocErrors) {
-	if doc.Raw == "" {
-		return errors
-	}
-
-	if phpdoc.IsSuspicious([]byte(doc.Raw)) {
-		errors.pushLint(
-			PHPDocLine(n, 1),
-			"Multiline PHPDoc comment should start with /**, not /*",
-		)
-	}
-
-	actualParamNames := make(map[string]struct{}, len(actualParams))
-	for _, p := range actualParams {
-		p := p.(*ir.Parameter)
-		actualParamNames[p.Variable.Name] = struct{}{}
-	}
-
-	var curParam int
-
-	for _, rawPart := range doc.Parsed {
-		d.checkPHPDocRef(n, rawPart)
-
-		if rawPart.Name() == "return" {
-			part := rawPart.(*phpdoc.TypeCommentPart)
-
-			converted := phpdoctypes.ToRealType(d.ctx.typeNormalizer.ClassFQNProvider(), part.Type)
-
-			if converted.Warning != "" {
-				errors.pushType(
-					PHPDocLineField(n, part.Line(), 1),
-					converted.Warning,
-				)
-			}
-
-			returnType := types.NewMapWithNormalization(d.ctx.typeNormalizer, converted.Types)
-
-			if returnType.Contains("void") && returnType.Len() > 1 {
-				errors.pushType(
-					PHPDocLineField(n, part.Line(), 1),
-					"Void type can only be used as a standalone type for the return type",
-				)
-			}
-
-			d.checkUndefinedClassesInPHPDoc(n, returnType, part)
-			continue
-		}
-
-		// Rest is for @param handling.
+func (d *rootWalker) getClass() meta.ClassInfo {
+	var m meta.ClassesMap
 
-		if rawPart.Name() != "param" {
-			continue
+	if d.ctx.st.IsTrait {
+		if d.meta.Traits.H == nil {
+			d.meta.Traits = meta.NewClassesMap()
 		}
-
-		part := rawPart.(*phpdoc.TypeVarCommentPart)
-		switch {
-		case part.Var == "":
-			errors.pushLint(
-				PHPDocLineField(n, part.Line(), 1),
-				"Malformed @param tag (maybe var is missing?)",
-			)
-
-		case part.Type.IsEmpty():
-			errors.pushLint(
-				PHPDocLineField(n, part.Line(), 1),
-				"Malformed @param %s tag (maybe type is missing?)", part.Var,
-			)
-
-			continue
+		m = d.meta.Traits
+	} else {
+		if d.meta.Classes.H == nil {
+			d.meta.Classes = meta.NewClassesMap()
 		}
+		m = d.meta.Classes
+	}
 
-		if part.VarIsFirst {
-			// Phpstorm gives the same message.
-			errors.pushLint(
-				PHPDocLine(n, part.Line()),
-				"Non-canonical order of variable and type",
-			)
+	cl, ok := m.Get(d.ctx.st.CurrentClass)
+	if !ok {
+		var flags meta.ClassFlags
+		if d.ctx.st.IsInterface {
+			flags = meta.ClassInterface
 		}
 
-		variable := part.Var
-		if !strings.HasPrefix(variable, "$") {
-			if len(actualParams) > curParam {
-				variable = actualParams[curParam].(*ir.Parameter).Variable.Name
-			}
-		}
-		if _, ok := actualParamNames[strings.TrimPrefix(variable, "$")]; !ok {
-			errors.pushLint(
-				PHPDocLineField(n, part.Line(), 2),
-				"@param for non-existing argument %s", variable,
-			)
-			continue
+		cl = meta.ClassInfo{
+			Pos:              d.getElementPos(d.currentClassNodeStack.Current()),
+			Name:             d.ctx.st.CurrentClass,
+			Flags:            flags,
+			Parent:           d.ctx.st.CurrentParentClass,
+			ParentInterfaces: d.ctx.st.CurrentParentInterfaces,
+			Interfaces:       make(map[string]struct{}),
+			Traits:           make(map[string]struct{}),
+			Methods:          meta.NewFunctionsMap(),
+			Properties:       make(meta.PropertiesMap),
+			Constants:        make(meta.ConstantsMap),
 		}
 
-		curParam++
-
-		converted := phpdoctypes.ToRealType(d.ctx.typeNormalizer.ClassFQNProvider(), part.Type)
-
-		if converted.Warning != "" {
-			errors.pushType(
-				PHPDocLineField(n, part.Line(), 1),
-				converted.Warning,
-			)
-		}
+		m.Set(d.ctx.st.CurrentClass, cl)
+	}
 
-		var param phpdoctypes.Param
-		param.Typ = types.NewMapWithNormalization(d.ctx.typeNormalizer, converted.Types)
+	return cl
+}
 
-		if param.Typ.Contains("void") {
-			errors.pushType(
-				PHPDocLineField(n, part.Line(), 1),
-				"Void type can only be used as a standalone type for the return type",
-			)
+func (d *rootWalker) parseStartPos(pos *position.Position) (startLn []byte, startChar int) {
+	if pos.StartLine >= 1 && d.file.NumLines() > pos.StartLine {
+		startLn = d.file.Line(pos.StartLine - 1)
+		p := d.file.LinePosition(pos.StartLine - 1)
+		if pos.StartPos > p {
+			startChar = pos.StartPos - p
 		}
-
-		d.checkUndefinedClassesInPHPDoc(n, param.Typ, part)
 	}
 
-	return errors
+	return startLn, startChar
 }
 
-// Parse type info, e.g. "string" in "someFunc() : string { ... }".
-func (d *rootWalker) parseTypeHintNode(n ir.Node) (typ types.Map, ok bool) {
-	if n == nil {
-		return types.Map{}, false
-	}
+func (d *rootWalker) getElementPos(n ir.Node) meta.ElementPosition {
+	pos := ir.GetPosition(n)
+	_, startChar := d.parseStartPos(pos)
 
-	typesMap := types.NormalizedTypeHintTypes(d.ctx.typeNormalizer, n)
-
-	return typesMap, !typesMap.Empty()
-}
-
-func (d *rootWalker) checkTypeHintNode(n ir.Node, place string) {
-	if !d.metaInfo().IsIndexingComplete() || n == nil {
-		return
-	}
-
-	// We need to check this part without normalization, since
-	// otherwise parent will be replaced with the class name.
-	typeList := types.TypeHintTypes(n)
-	for _, typ := range typeList {
-		if typ.Elem == "parent" && d.ctx.st.CurrentClass != "" {
-			if d.ctx.st.CurrentParentClass == "" {
-				d.Report(n, LevelError, "typeHint", "Cannot use 'parent' typehint when current class has no parent")
-			}
-		}
-	}
-
-	_, inTrait := d.currentClassNodeStack.Current().(*ir.TraitStmt)
-
-	typesMap := types.NewMapWithNormalization(d.ctx.typeNormalizer, typeList)
-
-	typesMap.Iterate(func(typ string) {
-		if types.IsClass(typ) {
-			className := typ
-
-			_, hasTrait := d.metaInfo().GetTrait(className)
-			if hasTrait && !inTrait {
-				d.Report(n, LevelWarning, "badTraitUse", "Cannot use trait %s as a typehint for %s", strings.TrimPrefix(className, `\`), place)
-			}
-
-			class, hasClass := d.metaInfo().GetClass(className)
-
-			if !hasClass && !hasTrait {
-				d.Report(n, LevelError, "undefinedClass",
-					"Class or interface named %s does not exist", className,
-				)
-			}
-
-			d.checkNameCase(n, className, class.Name)
-		}
-	})
-}
-
-// callbackParamByIndex returns the description of the parameter for the function by its index.
-func (d *rootWalker) callbackParamByIndex(param ir.Node, argType types.Map) meta.FuncParam {
-	p := param.(*ir.Parameter)
-	v := p.Variable
-
-	var typ types.Map
-	tp, ok := d.parseTypeHintNode(p.VariableType)
-	if ok {
-		typ = tp
-	} else {
-		typ = argType.Map(types.WrapElemOf)
-	}
-
-	arg := meta.FuncParam{
-		IsRef: p.ByRef,
-		Name:  v.Name,
-		Typ:   typ,
-	}
-	return arg
-}
-
-func (d *rootWalker) parseFuncArgsForCallback(params []ir.Node, sc *meta.Scope, closureSolver *solver.ClosureCallerInfo) (res parseFuncParamsResult) {
-	countParams := len(params)
-	minArgs := countParams
-	if countParams == 0 {
-		return res
-	}
-	args := make([]meta.FuncParam, countParams)
-
-	switch closureSolver.Name {
-	case `\usort`, `\uasort`, `\array_reduce`:
-		args[0] = d.callbackParamByIndex(params[0], closureSolver.ArgTypes[0])
-		if countParams > 1 {
-			args[1] = d.callbackParamByIndex(params[1], closureSolver.ArgTypes[0])
-		}
-	case `\array_walk`, `\array_walk_recursive`, `\array_filter`:
-		args[0] = d.callbackParamByIndex(params[0], closureSolver.ArgTypes[0])
-	case `\array_map`:
-		args[0] = d.callbackParamByIndex(params[0], closureSolver.ArgTypes[1])
-	}
-
-	for i, param := range params {
-		p := param.(*ir.Parameter)
-		v := p.Variable
-		var typ types.Map
-		if i < len(args) {
-			typ = args[i].Typ
-		} else {
-			typ = types.MixedType
-		}
-
-		sc.AddVarName(v.Name, typ, "param", meta.VarAlwaysDefined)
-	}
-
-	return parseFuncParamsResult{
-		params:         args,
-		minParamsCount: minArgs,
-	}
-}
-
-type parseFuncParamsResult struct {
-	params         []meta.FuncParam
-	paramsTypeHint map[string]types.Map
-	minParamsCount int
-}
-
-func (d *rootWalker) parseFuncParams(params []ir.Node, phpDocParamsTypes phpdoctypes.ParamsMap, sc *meta.Scope, closureSolver *solver.ClosureCallerInfo) (res parseFuncParamsResult) {
-	if len(params) == 0 {
-		return res
-	}
-
-	minArgs := 0
-	args := make([]meta.FuncParam, 0, len(params))
-	typeHints := make(map[string]types.Map, len(params))
-
-	if closureSolver != nil && solver.IsSupportedFunction(closureSolver.Name) {
-		return d.parseFuncArgsForCallback(params, sc, closureSolver)
-	}
-
-	for _, param := range params {
-		p := param.(*ir.Parameter)
-		v := p.Variable
-		phpDocType := phpDocParamsTypes[v.Name]
-
-		if !phpDocType.Typ.Empty() {
-			sc.AddVarName(v.Name, phpDocType.Typ, "param", meta.VarAlwaysDefined)
-		}
-
-		paramTyp := phpDocType.Typ
-
-		if p.DefaultValue == nil && !phpDocType.Optional && !p.Variadic {
-			minArgs++
-		}
-
-		if p.VariableType != nil {
-			typeHintType, ok := d.parseTypeHintNode(p.VariableType)
-			if ok {
-				paramTyp = typeHintType
-			}
-
-			typeHints[v.Name] = typeHintType
-		} else if paramTyp.Empty() && p.DefaultValue != nil {
-			paramTyp = solver.ExprTypeLocal(sc, d.ctx.st, p.DefaultValue)
-			// For the type resolver default value can look like a
-			// precise source of information (e.g. "false" is a precise bool),
-			// but it's not assigned unconditionally.
-			// If explicit argument is provided, that parameter can have
-			// almost any type possible.
-			paramTyp.MarkAsImprecise()
-		}
-
-		if p.Variadic {
-			paramTyp = paramTyp.Map(types.WrapArrayOf)
-		}
-
-		sc.AddVarName(v.Name, paramTyp, "param", meta.VarAlwaysDefined)
-
-		par := meta.FuncParam{
-			Typ:   paramTyp.Immutable(),
-			IsRef: p.ByRef,
-		}
-
-		par.Name = v.Name
-		args = append(args, par)
-	}
-
-	return parseFuncParamsResult{
-		params:         args,
-		paramsTypeHint: typeHints,
-		minParamsCount: minArgs,
-	}
-}
-
-func (d *rootWalker) typeHintHasMoreAccurateType(typeHintType, phpDocType types.Map) bool {
-	// If is not array typehint.
-	if !typeHintType.IsLazyArrayOf("mixed") {
-		return true
-	}
-
-	// If has more accurate type.
-	if !phpDocType.Empty() {
-		return true
-	}
-
-	return false
-}
-
-func (d *rootWalker) checkCommentMisspellings(n ir.Node, s string) {
-	// Try to avoid checking for symbol names and references.
-	d.checkMisspellings(n, s, "misspellComment", utils.IsCapitalized)
-}
-
-func (d *rootWalker) checkVarnameMisspellings(n ir.Node, s string) {
-	d.checkMisspellings(n, s, "misspellName", func(string) bool {
-		return false
-	})
-}
-
-func (d *rootWalker) checkIdentMisspellings(n *ir.Identifier) {
-	// Before PHP got context-sensitive lexer, it was common to use
-	// method names to avoid parsing errors.
-	// We can't suggest a fix that leads to a parsing error.
-	// To avoid false positives, skip PHP keywords.
-	d.checkMisspellings(n, n.Value, "misspellName", utils.IsPHPKeyword)
-}
-
-func (d *rootWalker) checkMisspellings(n ir.Node, s string, label string, skip func(string) bool) {
-	if !d.metaInfo().IsIndexingComplete() {
-		return
-	}
-	if d.config.TypoFixer == nil {
-		return
-	}
-	_, changes := d.config.TypoFixer.Replace(s)
-	for _, c := range changes {
-		if skip(c.Corrected) || skip(c.Original) {
-			continue
-		}
-		d.Report(n, LevelNotice, label, `"%s" is a misspelling of "%s"`, c.Original, c.Corrected)
-	}
-}
-
-func (d *rootWalker) enterFunction(fun *ir.FunctionStmt) bool {
-	nm := d.ctx.st.Namespace + `\` + fun.FunctionName.Value
-	pos := ir.GetPosition(fun)
-
-	if funcSize := pos.EndLine - pos.StartLine; funcSize > maxFunctionLines {
-		d.Report(fun.FunctionName, LevelNotice, "complexity", "Too big function: more than %d lines", maxFunctionLines)
-	}
-
-	if d.meta.Functions.H == nil {
-		d.meta.Functions = meta.NewFunctionsMap()
-	}
-
-	d.checkCommentMisspellings(fun.FunctionName, fun.Doc.Raw)
-	d.checkIdentMisspellings(fun.FunctionName)
-
-	// Indexing stage.
-	doc := phpdoctypes.Parse(fun.Doc, fun.Params, d.ctx.typeNormalizer)
-	moveShapesToContext(&d.ctx, doc.Shapes)
-	d.handleClosuresFromDoc(doc.Closures)
-
-	// Check stage.
-	errors := d.checkPHPDoc(fun, fun.Doc, fun.Params)
-	d.reportPHPDocErrors(errors)
-
-	phpDocReturnType := doc.ReturnType
-	phpDocParamTypes := doc.ParamTypes
-
-	sc := meta.NewScope()
-
-	returnTypeHint, ok := d.parseTypeHintNode(fun.ReturnType)
-	if ok && !doc.Inherit {
-		d.checkFuncReturnType(fun.FunctionName, fun.FunctionName.Value, returnTypeHint, phpDocReturnType)
-	}
-	d.checkTypeHintNode(fun.ReturnType, "return type")
-
-	funcParams := d.parseFuncParams(fun.Params, phpDocParamTypes, sc, nil)
-
-	d.checkFuncParams(fun.FunctionName, fun.Params, funcParams, phpDocParamTypes)
-
-	funcInfo := d.handleFuncStmts(funcParams.params, nil, fun.Stmts, sc)
-	actualReturnTypes := funcInfo.returnTypes
-	exitFlags := funcInfo.prematureExitFlags
-
-	returnTypes := functionReturnType(phpDocReturnType, returnTypeHint, actualReturnTypes)
-
-	var funcFlags meta.FuncFlags
-	if solver.SideEffectFreeFunc(d.scope(), d.ctx.st, nil, fun.Stmts) {
-		funcFlags |= meta.FuncPure
-	}
-	d.meta.Functions.Set(nm, meta.FuncInfo{
-		Params:       funcParams.params,
-		Name:         nm,
-		Pos:          d.getElementPos(fun),
-		Typ:          returnTypes.Immutable(),
-		MinParamsCnt: funcParams.minParamsCount,
-		Flags:        funcFlags,
-		ExitFlags:    exitFlags,
-		Doc:          doc.AdditionalInfo,
-	})
-
-	return false
-}
-
-func (d *rootWalker) handleClosuresFromDoc(closures types.ClosureMap) {
-	if d.meta.Functions.H == nil {
-		d.meta.Functions = meta.NewFunctionsMap()
-	}
-
-	for name, closureInfo := range closures {
-		var params []meta.FuncParam
-		for i, paramType := range closureInfo.ParamTypes {
-			params = append(params, meta.FuncParam{
-				Name: fmt.Sprintf("closure param #%d", i),
-				Typ:  types.NewMapWithNormalization(d.ctx.typeNormalizer, paramType),
-			})
-		}
-
-		d.meta.Functions.Set(name, meta.FuncInfo{
-			Params:       params,
-			Name:         name,
-			Typ:          types.NewMapWithNormalization(d.ctx.typeNormalizer, closureInfo.ReturnType),
-			MinParamsCnt: len(closureInfo.ParamTypes),
-		})
+	return meta.ElementPosition{
+		Filename:  d.ctx.st.CurrentFile,
+		Character: int32(startChar),
+		Line:      int32(pos.StartLine),
+		EndLine:   int32(pos.EndLine),
+		Length:    int32(pos.EndPos - pos.StartPos),
 	}
 }
 
-func (d *rootWalker) checkFuncParams(funcName *ir.Identifier, params []ir.Node, funcParams parseFuncParamsResult, phpDocParamTypes phpdoctypes.ParamsMap) {
-	for _, param := range params {
-		d.checkFuncParam(param.(*ir.Parameter))
+// nodeText is used to get the string that represents the
+// passed node more efficiently than irutil.FmtNode.
+func (d *rootWalker) nodeText(n ir.Node) string {
+	pos := ir.GetPosition(n)
+	from, to := pos.StartPos, pos.EndPos
+	src := d.file.Contents()
+	// Taking a node from the source code preserves the original formatting
+	// and is more efficient than printing it.
+	if (from >= 0 && from < len(src)) && (to >= 0 && to < len(src)) {
+		return string(src[from:to])
 	}
-
-	d.checkParamsTypeHint(funcName, funcParams, phpDocParamTypes)
+	// If we can't take node out of the source text, print it.
+	return irutil.FmtNode(n)
 }
 
-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
-		}
+// Report part.
 
-		if !d.typeHintHasMoreAccurateType(typeHintType, phpDocType) {
-			d.Report(funcName, LevelNotice, "typeHint", "Specify the type for the parameter $%s in PHPDoc, 'array' type hint too generic", param)
+// ReportPHPDoc registers a single report message about some found problem in PHPDoc.
+func (d *rootWalker) ReportPHPDoc(phpDocLocation PHPDocLocation, level int, checkName, msg string, args ...interface{}) {
+	if phpDocLocation.RelativeLine {
+		doc, ok := irutil.FindPHPDoc(phpDocLocation.Node, true)
+		if !ok {
+			// If PHPDoc for some reason was not found, give a warning to the node.
+			d.Report(phpDocLocation.Node, level, checkName, msg, args...)
+			return
 		}
-	}
-}
-
-func (d *rootWalker) checkFuncReturnType(fun ir.Node, funcName string, returnTypeHint, phpDocReturnType types.Map) {
-	if !d.typeHintHasMoreAccurateType(returnTypeHint, phpDocReturnType) {
-		d.Report(fun, LevelNotice, "typeHint", "Specify the return type for the function %s in PHPDoc, 'array' type hint too generic", funcName)
-	}
-}
-
-func (d *rootWalker) checkFuncParam(p *ir.Parameter) {
-	d.checkVarnameMisspellings(p, p.Variable.Name)
 
-	// TODO(quasilyte): DefaultValue can only contain constant expressions.
-	// Could run special check over them to detect the potential fatal errors.
-	irutil.Inspect(p.DefaultValue, func(w ir.Node) bool {
-		if n, ok := w.(*ir.ArrayExpr); ok && !n.ShortSyntax {
-			d.Report(n, LevelNotice, "arraySyntax", "Use the short form '[]' instead of the old 'array()'")
-			d.addFixForArray(n)
-		}
-		return true
-	})
+		countPHPDocLines := strings.Count(doc, "\n") + 1
 
-	d.checkTypeHintFunctionParam(p)
-}
+		nodePos := ir.GetPosition(phpDocLocation.Node)
+		if nodePos == nil {
+			// If position for some reason was not found, give a warning to the node.
+			d.Report(phpDocLocation.Node, level, checkName, msg, args...)
+			return
+		}
 
-func (d *rootWalker) addFixForArray(arr *ir.ArrayExpr) {
-	if !d.config.ApplyQuickFixes {
-		return
+		// 1| <?php
+		// 2|
+		// 3| /**
+		// 4|  * Comment
+		// 5|  * @param int $a    <- phpDocLocation.Line == 3
+		// 6|  */                 <- countPHPDocLines == 4
+		// 7| function f($a) {}   <- nodePos.StartLine == 7
+		//
+		// countPHPDocLines - phpDocLocation.Line = 1
+		// nodePos.StartLine - 1 = 6
+		// 6 - 1 = 5 (number of the required line relative to one)
+		// 5 - 1 = 4 (number of the required line relative to zero)
+		phpDocLocation.Line = nodePos.StartLine - (countPHPDocLines - phpDocLocation.Line) - 1 - 1
 	}
 
-	from := arr.Position.StartPos
-	to := arr.Position.EndPos
-	have := d.file.Contents()[from:to]
-	have = bytes.TrimPrefix(have, []byte("array"))
-	have = bytes.TrimSpace(have)
-	have = bytes.TrimPrefix(have, []byte("("))
-	have = bytes.TrimSuffix(have, []byte(")"))
-
-	d.addQuickFix("arraySyntax", quickfix.TextEdit{
-		StartPos:    arr.Position.StartPos,
-		EndPos:      arr.Position.EndPos,
-		Replacement: fmt.Sprintf("[%s]", string(have)),
-	})
-}
-
-func (d *rootWalker) checkTypeHintFunctionParam(p *ir.Parameter) {
-	if !d.metaInfo().IsIndexingComplete() {
+	if phpDocLocation.Line < 0 || phpDocLocation.Line >= d.file.NumLines() {
+		d.Report(phpDocLocation.Node, level, checkName, msg, args...)
 		return
 	}
 
-	d.checkTypeHintNode(p.VariableType, "parameter type")
-}
+	contextLine := d.file.Line(phpDocLocation.Line)
 
-func (d *rootWalker) enterFunctionCall(s *ir.FunctionCallExpr) bool {
-	nm, ok := s.Function.(*ir.Name)
-	if !ok {
-		return true
+	lineWithoutBeginning := contextLine
+	// For the case when we give a warning about the wrong start
+	// of PHPDoc (/* instead /**), it is not necessary to delete characters.
+	if !bytes.Contains(contextLine, []byte("/*")) || bytes.Contains(contextLine, []byte("/**")) {
+		lineWithoutBeginning = bytes.TrimLeft(contextLine, "/ *")
 	}
 
-	name := strings.TrimPrefix(nm.Value, `\`)
+	shiftFromStart := len(contextLine) - len(lineWithoutBeginning)
 
-	if d.ctx.st.Namespace == `\PHPSTORM_META` && name == `override` {
-		return d.handleOverride(s)
+	parts := bytes.Fields(lineWithoutBeginning)
+	if phpDocLocation.Field >= len(parts) {
+		phpDocLocation.Field = 0
+		phpDocLocation.WholeLine = true
 	}
 
-	if name == "define" {
-		d.handleDefineCall(s)
-	}
+	var startChar int
+	var endChar int
 
-	return true
-}
+	if phpDocLocation.WholeLine {
+		startChar = shiftFromStart
+		endChar = len(contextLine)
+	} else {
+		part := parts[phpDocLocation.Field]
+		shiftStart := bytes.Index(lineWithoutBeginning, part)
+		shiftEnd := shiftStart + len(part)
 
-func (d *rootWalker) handleDefineCall(s *ir.FunctionCallExpr) {
-	if len(s.Args) < 2 {
-		return
+		startChar = shiftFromStart + shiftStart
+		endChar = shiftFromStart + shiftEnd
 	}
 
-	arg := s.Arg(0)
-
-	str, ok := arg.Expr.(*ir.String)
-	if !ok {
-		return
+	if endChar == len(contextLine) && bytes.HasSuffix(contextLine, []byte("\r")) {
+		endChar--
 	}
 
-	valueArg := s.Arg(1)
-
-	if d.meta.Constants == nil {
-		d.meta.Constants = make(meta.ConstantsMap)
+	loc := ir.Location{
+		StartLine: phpDocLocation.Line,
+		EndLine:   phpDocLocation.Line,
+		StartChar: startChar,
+		EndChar:   endChar,
 	}
 
-	value := constfold.Eval(d.ctx.st, valueArg)
-
-	d.meta.Constants[`\`+strings.TrimFunc(str.Value, utils.IsQuote)] = meta.ConstInfo{
-		Pos:   d.getElementPos(s),
-		Typ:   solver.ExprTypeLocal(d.scope(), d.ctx.st, valueArg.Expr),
-		Value: value,
-	}
+	d.ReportLocation(loc, level, checkName, msg, args...)
 }
 
-// Handle e.g. "override(\array_shift(0), elementType(0));"
-// which means "return type of array_shift() is the type of element of first function parameter"
-func (d *rootWalker) handleOverride(s *ir.FunctionCallExpr) bool {
-	if len(s.Args) != 2 {
-		return true
-	}
-
-	arg0 := s.Arg(0)
-	arg1 := s.Arg(1)
+func (d *rootWalker) Report(n ir.Node, level int, checkName, msg string, args ...interface{}) {
+	var pos position.Position
 
-	fc0, ok := arg0.Expr.(*ir.FunctionCallExpr)
-	if !ok {
-		return true
+	if n == nil {
+		// Hack to parse syntax error message from php-parser.
+		if strings.Contains(msg, "syntax error") && strings.Contains(msg, " at line ") {
+			// it is in form "Syntax error: syntax error: unexpected '*' at line 4"
+			if lastIdx := strings.LastIndexByte(msg, ' '); lastIdx > 0 {
+				lineNumStr := msg[lastIdx+1:]
+				lineNum, err := strconv.Atoi(lineNumStr)
+				if err == nil {
+					pos.StartLine = lineNum
+					pos.EndLine = lineNum
+					msg = msg[0:lastIdx]
+					msg = strings.TrimSuffix(msg, " at line")
+				}
+			}
+		}
+	} else {
+		nodePos := ir.GetPosition(n)
+		if nodePos == nil {
+			return
+		}
+		pos = *nodePos
 	}
 
-	fc1, ok := arg1.Expr.(*ir.FunctionCallExpr)
-	if !ok {
-		return true
-	}
+	var loc ir.Location
 
-	fnNameNode, ok := fc0.Function.(*ir.Name)
-	if !ok || !fnNameNode.IsFullyQualified() {
-		return true
-	}
+	loc.StartLine = pos.StartLine - 1
+	loc.EndLine = pos.EndLine - 1
+	loc.StartChar = pos.StartPos
+	loc.EndChar = pos.EndPos
 
-	overrideNameNode, ok := fc1.Function.(*ir.Name)
-	if !ok {
-		return true
+	if pos.StartLine >= 1 && d.file.NumLines() >= pos.StartLine {
+		p := d.file.LinePosition(pos.StartLine - 1)
+		if pos.StartPos >= p {
+			loc.StartChar = pos.StartPos - p
+		}
 	}
 
-	if len(fc1.Args) != 1 {
-		return true
+	if pos.EndLine >= 1 && d.file.NumLines() >= pos.EndLine {
+		p := d.file.LinePosition(pos.EndLine - 1)
+		if pos.EndPos >= p {
+			loc.EndChar = pos.EndPos - p
+		}
 	}
 
-	fc1Arg0 := fc1.Arg(0)
+	d.ReportLocation(loc, level, checkName, msg, args...)
+}
 
-	argNumNode, ok := fc1Arg0.Expr.(*ir.Lnumber)
-	if !ok {
-		return true
+func (d *rootWalker) ReportLocation(loc ir.Location, level int, checkName, msg string, args ...interface{}) {
+	if !d.metaInfo().IsIndexingComplete() {
+		return
 	}
-
-	argNum, err := strconv.Atoi(argNumNode.Value)
-	if err != nil {
-		return true
+	if d.file.AutoGenerated() && !d.config.CheckAutoGenerated {
+		return
 	}
-
-	var overrideTyp meta.OverrideType
-	switch {
-	case overrideNameNode.Value == `type`:
-		overrideTyp = meta.OverrideArgType
-	case overrideNameNode.Value == `elementType`:
-		overrideTyp = meta.OverrideElementType
-	default:
-		return true
+	// We don't report anything if linter was disabled by a
+	// successful @linter disable, unless it's the linterError.
+	if d.linterDisabled && checkName != "linterError" {
+		return
 	}
 
-	fnName := fnNameNode.Value
-
-	if d.meta.FunctionOverrides == nil {
-		d.meta.FunctionOverrides = make(meta.FunctionsOverrideMap)
+	if !d.checkersFilter.IsEnabledReport(checkName, d.ctx.st.CurrentFile) {
+		return
 	}
 
-	d.meta.FunctionOverrides[fnName] = meta.FuncInfoOverride{
-		OverrideType: overrideTyp,
-		ArgNum:       argNum,
+	if loc.StartLine < 0 || loc.StartLine >= d.file.NumLines() {
+		return
 	}
 
-	return true
-}
+	contextLine := d.file.Line(loc.StartLine)
 
-func (d *rootWalker) enterConstList(lst *ir.ConstListStmt) bool {
-	if d.meta.Constants == nil {
-		d.meta.Constants = make(meta.ConstantsMap)
+	var hash uint64
+	// If baseline is not enabled, don't waste time on hash computations.
+	if d.config.ComputeBaselineHashes {
+		hash = d.reportHash(&loc, contextLine, checkName, msg)
+		if count := d.ctx.baseline.Count(hash); count >= 1 {
+			if d.ctx.hashCounters == nil {
+				d.ctx.hashCounters = make(map[uint64]int)
+			}
+			d.ctx.hashCounters[hash]++
+			if d.ctx.hashCounters[hash] <= count {
+				return
+			}
+		}
 	}
 
-	for _, sNode := range lst.Consts {
-		s := sNode.(*ir.ConstantStmt)
-
-		value := constfold.Eval(d.ctx.st, s.Expr)
+	d.reports = append(d.reports, &Report{
+		CheckName: checkName,
+		Context:   string(contextLine),
+		StartChar: loc.StartChar,
+		EndChar:   loc.EndChar,
+		Line:      loc.StartLine + 1,
+		Level:     level,
+		Filename:  strings.ReplaceAll(d.ctx.st.CurrentFile, "\\", "/"), // To make output stable between platforms, see #572
+		Message:   fmt.Sprintf(msg, args...),
+		Hash:      hash,
+	})
+}
 
-		id := s.ConstantName
-		nm := d.ctx.st.Namespace + `\` + id.Value
+// reportHash computes the ReportLocation signature hash for the baseline.
+func (d *rootWalker) reportHash(loc *ir.Location, contextLine []byte, checkName, msg string) uint64 {
+	// Since we store class::method scope, renaming a class would cause baseline
+	// invalidation for the entire class. But this is not an issue, since in
+	// a modern PHP class name always should map to a filename.
+	// If we renamed a class, we probably renamed the file as well, so
+	// the baseline would be invalidated anyway.
+	//
+	// We still get all the benefits by using method prefix: it's common
+	// for different classes to have methods with similar name. We don't
+	// want such methods to be considered as a single "scope".
+	scope := "file"
+	switch {
+	case d.ctx.st.CurrentClass != "" && d.ctx.st.CurrentFunction != "":
+		scope = d.ctx.st.CurrentClass + "::" + d.ctx.st.CurrentFunction
+	case d.ctx.st.CurrentFunction != "":
+		scope = d.ctx.st.CurrentFunction
+	}
 
-		d.meta.Constants[nm] = meta.ConstInfo{
-			Pos:   d.getElementPos(s),
-			Typ:   solver.ExprTypeLocal(d.scope(), d.ctx.st, s.Expr),
-			Value: value,
+	var prevLine []byte
+	var nextLine []byte
+	// Adjacent lines are only included when using non-conservative baseline.
+	if !d.config.ConservativeBaseline {
+		// Lines are 1-based, indexes are 0-based.
+		// If this function is called, we expect that lines[index] exists.
+		index := loc.StartLine - 1
+		if index >= 1 {
+			prevLine = d.file.Line(index - 1)
+		}
+		if d.file.NumLines() > index+1 {
+			nextLine = d.file.Line(index + 1)
 		}
 	}
 
-	return false
-}
-
-// LeaveNode is invoked after node process
-func (d *rootWalker) LeaveNode(n ir.Node) {
-	for _, c := range d.custom {
-		c.BeforeLeaveNode(n)
-	}
+	// Observation: using a base file name instead of its full name does not
+	// introduce any "bad collisions", because we have a "scope" part
+	// that cuts the risk by a big margin.
+	//
+	// One benefit is that it makes the baseline contents more position-independent.
+	// We don't need to know a source root folder to truncate it.
+	//
+	// Moves like Foo/Bar/User.php -> Common/User.php do not invalidate the
+	// suppress base. This is not an obvious win, but it may be a good
+	// compromise to solve a use case where a class file is being moved.
+	// For classes, filename is unlikely to be changed during this file move operation.
+	//
+	// It can't handle file duplication when Foo/Bar/User.php is *copied* to a new location.
+	// It may be useful to give warnings to both *old* and *new* copies of the duplicated
+	// code since some bugs should be fixed in both places.
+	// We'll see how it goes.
+	filename := filepath.Base(d.ctx.st.CurrentFile)
 
-	switch n.(type) {
-	case *ir.ClassStmt, *ir.InterfaceStmt, *ir.TraitStmt, *ir.AnonClassExpr:
-		d.getClass() // populate classes map
+	d.ctx.scratchBuf.Reset()
+	return baseline.ReportHash(&d.ctx.scratchBuf, baseline.HashFields{
+		Filename:  filename,
+		PrevLine:  bytes.TrimSuffix(prevLine, []byte("\r")),
+		StartLine: bytes.TrimSuffix(contextLine, []byte("\r")),
+		NextLine:  bytes.TrimSuffix(nextLine, []byte("\r")),
+		CheckName: checkName,
+		Message:   msg,
+		Scope:     scope,
+	})
+}
 
-		d.currentClassNodeStack.Pop()
+func (d *rootWalker) reportUndefinedVariable(v ir.Node, maybeHave bool, path irutil.NodePath) {
+	sv, ok := v.(*ir.SimpleVar)
+	if !ok {
+		d.Report(v, LevelWarning, "undefinedVariable", "Unknown variable variable %s used",
+			utils.NameNodeToString(v))
+		return
 	}
 
-	state.LeaveNode(d.ctx.st, n)
-
-	for _, c := range d.custom {
-		c.AfterLeaveNode(n)
+	if _, ok := superGlobals[sv.Name]; ok {
+		return
 	}
-}
 
-func (d *rootWalker) addQuickFix(checkName string, fix quickfix.TextEdit) {
-	if !d.checkersFilter.IsEnabledReport(checkName, d.ctx.st.CurrentFile) {
+	// For the following cases, we do not give warnings,
+	// as they check for the presence of this variable.
+	// $b = $a ?? 100;
+	// $b = isset($a) ? $a : 100;
+	needWarn := !utils.InCoalesceOrIsset(path)
+	if !needWarn {
 		return
 	}
 
-	d.ctx.fixes = append(d.ctx.fixes, fix)
+	if maybeHave {
+		d.Report(sv, LevelWarning, "maybeUndefined", "Possibly undefined variable $%s", sv.Name)
+	} else {
+		d.Report(sv, LevelError, "undefinedVariable", "Cannot find referenced variable $%s", sv.Name)
+	}
 }
 
+// Rules part.
+
 func (d *rootWalker) runRules(n ir.Node, sc *meta.Scope, rlist []rules.Rule) {
 	for i := range rlist {
 		rule := &rlist[i]
@@ -2227,21 +1641,6 @@ func (d *rootWalker) runRules(n ir.Node, sc *meta.Scope, rlist []rules.Rule) {
 	}
 }
 
-// nodeText is used to get the string that represents the
-// passed node more efficiently than irutil.FmtNode.
-func (d *rootWalker) nodeText(n ir.Node) string {
-	pos := ir.GetPosition(n)
-	from, to := pos.StartPos, pos.EndPos
-	src := d.file.Contents()
-	// Taking a node from the source code preserves the original formatting
-	// and is more efficient than printing it.
-	if (from >= 0 && from < len(src)) && (to >= 0 && to < len(src)) {
-		return string(src[from:to])
-	}
-	// If we can't take node out of the source text, print it.
-	return irutil.FmtNode(n)
-}
-
 func (d *rootWalker) renderRuleMessage(msg string, n ir.Node, m phpgrep.MatchData, truncate bool) string {
 	// "$$" stands for the entire matched node, like $0 in regexp.
 	if strings.Contains(msg, "$$") {
@@ -2352,255 +1751,3 @@ func (d *rootWalker) checkFilterSet(m *phpgrep.MatchData, sc *meta.Scope, filter
 
 	return true
 }
-
-func (d *rootWalker) checkTraitImplemented(classNode, name ir.Node, nameUsed string) {
-	if !d.metaInfo().IsIndexingComplete() {
-		return
-	}
-	trait, ok := d.metaInfo().GetTrait(nameUsed)
-	if !ok {
-		d.reportUndefinedTrait(name, nameUsed)
-		return
-	}
-	d.checkImplemented(classNode, name, nameUsed, trait)
-}
-
-func (d *rootWalker) checkClassInherit(classNode, extendsClassNameNode ir.Node, nameUsed string) {
-	if !d.metaInfo().IsIndexingComplete() {
-		return
-	}
-
-	class, ok := d.metaInfo().GetClass(nameUsed)
-	if !ok {
-		d.reportUndefinedClass(extendsClassNameNode, nameUsed)
-		return
-	}
-
-	d.checkClassExtends(extendsClassNameNode, class)
-	d.checkImplemented(classNode, extendsClassNameNode, nameUsed, class)
-}
-
-func (d *rootWalker) checkClassExtends(extendsClassNameNode ir.Node, otherClass meta.ClassInfo) {
-	if otherClass.IsFinal() {
-		currentClass := d.getClass()
-		d.Report(extendsClassNameNode, LevelError, "invalidExtendClass", "Class %s may not inherit from final class %s", currentClass.Name, otherClass.Name)
-	}
-}
-
-func (d *rootWalker) checkClassImplemented(classNode, extendsClassNameNode ir.Node, nameUsed string) {
-	if !d.metaInfo().IsIndexingComplete() {
-		return
-	}
-	class, ok := d.metaInfo().GetClass(nameUsed)
-	if !ok {
-		d.reportUndefinedClass(extendsClassNameNode, nameUsed)
-		return
-	}
-	d.checkImplemented(classNode, extendsClassNameNode, nameUsed, class)
-}
-
-func (d *rootWalker) checkIfaceImplemented(classNode, name ir.Node, nameUsed string) {
-	d.checkClassImplemented(classNode, name, nameUsed)
-}
-
-func (d *rootWalker) checkImplemented(classNode, name ir.Node, nameUsed string, otherClass meta.ClassInfo) {
-	cl := d.getClass()
-	if d.ctx.st.IsTrait || cl.IsAbstract() {
-		return
-	}
-	d.checkNameCase(name, nameUsed, otherClass.Name)
-	visited := make(map[string]struct{}, 4)
-	d.checkImplementedStep(classNode, name, nameUsed, otherClass, visited)
-}
-
-func (d *rootWalker) checkImplementedStep(classNode, name ir.Node, className string, otherClass meta.ClassInfo, visited map[string]struct{}) {
-	// TODO: check that method signatures are compatible?
-	if _, ok := visited[className]; ok {
-		return
-	}
-	visited[className] = struct{}{}
-	for _, ifaceMethod := range otherClass.Methods.H {
-		m, ok := solver.FindMethod(d.metaInfo(), d.ctx.st.CurrentClass, ifaceMethod.Name)
-		if !ok || !m.Implemented {
-			d.Report(name, LevelError, "unimplemented", "Class %s must implement %s::%s method",
-				d.ctx.st.CurrentClass, className, ifaceMethod.Name)
-			continue
-		}
-		if m.Info.Name != ifaceMethod.Name {
-			d.Report(name, LevelNotice, "nameMismatch", "%s::%s should be spelled as %s::%s",
-				d.ctx.st.CurrentClass, m.Info.Name, className, ifaceMethod.Name)
-
-		}
-		if ifaceMethod.IsFinal() && ifaceMethod.AccessLevel != meta.Private {
-			methodNode := irutil.FindClassMethodNode(classNode, ifaceMethod.Name)
-			if methodNode != nil {
-				d.Report(methodNode, LevelError, "methodSignatureMismatch",
-					"Method %s::%s is declared final and cannot be overridden",
-					otherClass.Name, ifaceMethod.Name)
-			}
-		}
-	}
-	for _, ifaceName := range otherClass.ParentInterfaces {
-		iface, ok := d.metaInfo().GetClass(ifaceName)
-		if ok {
-			d.checkImplementedStep(classNode, name, ifaceName, iface, visited)
-		}
-	}
-	if otherClass.Parent != "" {
-		class, ok := d.metaInfo().GetClass(otherClass.Parent)
-		if ok {
-			d.checkImplementedStep(classNode, name, otherClass.Parent, class, visited)
-		}
-	}
-}
-
-func (d *rootWalker) reportUndefinedClass(n ir.Node, name string) {
-	d.Report(n, LevelError, "undefinedClass", "Class or interface named %s does not exist", name)
-}
-
-func (d *rootWalker) reportUndefinedTrait(n ir.Node, name string) {
-	d.Report(n, LevelError, "undefinedTrait", "Trait named %s does not exist", name)
-}
-
-func (d *rootWalker) checkNameCase(n ir.Node, nameUsed, nameExpected string) {
-	if nameUsed == "" || nameExpected == "" {
-		return
-	}
-	if nameUsed != nameExpected {
-		d.Report(n, LevelWarning, "nameMismatch", "%s should be spelled %s",
-			nameUsed, nameExpected)
-	}
-}
-
-func (d *rootWalker) checkKeywordCase(n ir.Node, keyword string) {
-	toks := irutil.Keywords(n)
-	if toks == nil {
-		return
-	}
-
-	tok := toks[0]
-
-	switch n := n.(type) {
-	case *ir.YieldFromExpr:
-		d.compareKeywordWithTokenCase(n, toks[0], "yield")
-		d.compareKeywordWithTokenCase(n, toks[1], "from")
-
-	case *ir.ElseIfStmt:
-		if !n.Merged {
-			d.compareKeywordWithTokenCase(n, toks[0], "if")
-			d.compareKeywordWithTokenCase(n, toks[1], "else")
-		} else {
-			d.compareKeywordWithTokenCase(n, tok, "elseif")
-		}
-
-	default:
-		d.compareKeywordWithTokenCase(n, tok, keyword)
-	}
-}
-
-func (d *rootWalker) compareKeywordWithTokenCase(n ir.Node, tok *token.Token, keyword string) {
-	wantKwd := keyword
-	haveKwd := tok.Value
-	if wantKwd != string(haveKwd) {
-		d.Report(n, LevelWarning, "keywordCase", "Use %s instead of %s",
-			wantKwd, haveKwd)
-	}
-}
-
-func (d *rootWalker) parseClassPHPDoc(class ir.Node, doc phpdoc.Comment) classPHPDocParseResult {
-	var result classPHPDocParseResult
-
-	if doc.Raw == "" {
-		return result
-	}
-
-	// TODO: allocate maps lazily.
-	// Class may not have any @property or @method annotations.
-	// In that case we can handle avoid map allocations.
-	result.properties = make(meta.PropertiesMap)
-	result.methods = meta.NewFunctionsMap()
-
-	for _, part := range doc.Parsed {
-		d.checkPHPDocRef(class, part)
-		switch part.Name() {
-		case "property", "property-read", "property-write":
-			parseClassPHPDocProperty(class, &d.ctx, &result, part.(*phpdoc.TypeVarCommentPart))
-		case "method":
-			parseClassPHPDocMethod(class, &d.ctx, &result, part.(*phpdoc.RawCommentPart))
-		case "mixin":
-			parseClassPHPDocMixin(class, d.ctx.st, &result, part.(*phpdoc.RawCommentPart))
-		}
-	}
-
-	return result
-}
-
-func (d *rootWalker) beforeEnterFile() {
-	for _, c := range d.custom {
-		c.BeforeEnterFile()
-	}
-}
-
-func (d *rootWalker) afterLeaveFile() {
-	for _, c := range d.custom {
-		c.AfterLeaveFile()
-	}
-
-	if !d.metaInfo().IsIndexingComplete() {
-		for _, shape := range d.ctx.shapes {
-			props := make(meta.PropertiesMap)
-			for _, p := range shape.Props {
-				props[p.Key] = meta.PropertyInfo{
-					Typ:         types.NewMapWithNormalization(d.ctx.typeNormalizer, p.Types).Immutable(),
-					AccessLevel: meta.Public,
-				}
-			}
-			cl := meta.ClassInfo{
-				Name:       shape.Name,
-				Properties: props,
-				Flags:      meta.ClassShape,
-			}
-			if d.meta.Classes.H == nil {
-				d.meta.Classes = meta.NewClassesMap()
-			}
-			d.meta.Classes.Set(shape.Name, cl)
-		}
-	}
-}
-
-func (d *rootWalker) metaInfo() *meta.Info {
-	return d.ctx.st.Info
-}
-
-func (d *rootWalker) currentFunction() (meta.FuncInfo, bool) {
-	name := d.ctx.st.CurrentFunction
-	if name == "" {
-		return meta.FuncInfo{}, false
-	}
-
-	if d.ctx.st.CurrentClass != "" {
-		className, ok := solver.GetClassName(d.ctx.st, &ir.Name{Value: d.ctx.st.CurrentClass})
-		if !ok {
-			return meta.FuncInfo{}, false
-		}
-
-		method, ok := solver.FindMethod(d.ctx.st.Info, className, name)
-		if !ok {
-			return meta.FuncInfo{}, false
-		}
-
-		return method.Info, true
-	}
-
-	funcName, ok := solver.GetFuncName(d.ctx.st, &ir.Name{Value: name})
-	if !ok {
-		return meta.FuncInfo{}, false
-	}
-
-	fun, ok := d.ctx.st.Info.GetFunction(funcName)
-	if !ok {
-		return meta.FuncInfo{}, false
-	}
-
-	return fun, true
-}
diff --git a/src/linter/root_checker.go b/src/linter/root_checker.go
new file mode 100644
index 0000000..baee244
--- /dev/null
+++ b/src/linter/root_checker.go
@@ -0,0 +1,900 @@
+package linter
+
+import (
+	"strings"
+
+	"github.com/VKCOM/noverify/src/ir"
+	"github.com/VKCOM/noverify/src/ir/irutil"
+	"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"
+	"github.com/VKCOM/noverify/src/utils"
+	"github.com/VKCOM/noverify/src/workspace"
+	"github.com/VKCOM/php-parser/pkg/token"
+	"github.com/client9/misspell"
+)
+
+type rootChecker struct {
+	walker *rootWalker
+
+	file       *workspace.File
+	normalizer types.Normalizer
+	info       *meta.Info
+
+	scope  *meta.Scope
+	state  *meta.ClassParseState
+	parser *phpdoc.TypeParser
+
+	currentClassNodeStack *irutil.NodePath
+
+	// TypoFixer is a rule set for English typos correction.
+	// If nil, no misspell checking is performed.
+	// See github.com/client9/misspell for details.
+	typoFixer *misspell.Replacer
+
+	quickfix *QuickFixGenerator
+}
+
+func newRootChecker(walker *rootWalker, quickfix *QuickFixGenerator) *rootChecker {
+	c := &rootChecker{
+		file:                  walker.file,
+		walker:                walker,
+		normalizer:            walker.ctx.typeNormalizer,
+		info:                  walker.metaInfo(),
+		state:                 walker.ctx.st,
+		scope:                 walker.scope(),
+		parser:                walker.ctx.phpdocTypeParser,
+		currentClassNodeStack: &walker.currentClassNodeStack,
+		quickfix:              quickfix,
+	}
+	if walker.config != nil {
+		c.typoFixer = walker.config.TypoFixer
+	}
+	return c
+}
+
+func (r *rootChecker) CheckPHPDoc(n ir.Node, doc phpdoc.Comment, actualParams []ir.Node) (errors PHPDocErrors) {
+	if doc.Raw == "" {
+		return errors
+	}
+
+	if phpdoc.IsSuspicious([]byte(doc.Raw)) {
+		errors.pushLint(
+			PHPDocLine(n, 1),
+			"Multiline PHPDoc comment should start with /**, not /*",
+		)
+	}
+
+	actualParamNames := make(map[string]struct{}, len(actualParams))
+	for _, p := range actualParams {
+		p := p.(*ir.Parameter)
+		actualParamNames[p.Variable.Name] = struct{}{}
+	}
+
+	var curParam int
+
+	for _, rawPart := range doc.Parsed {
+		r.checkPHPDocRef(n, rawPart)
+
+		if rawPart.Name() == "return" {
+			part := rawPart.(*phpdoc.TypeCommentPart)
+
+			converted := phpdoctypes.ToRealType(r.normalizer.ClassFQNProvider(), part.Type)
+
+			if converted.Warning != "" {
+				errors.pushType(
+					PHPDocLineField(n, part.Line(), 1),
+					converted.Warning,
+				)
+			}
+
+			returnType := types.NewMapWithNormalization(r.normalizer, converted.Types)
+
+			if returnType.Contains("void") && returnType.Len() > 1 {
+				errors.pushType(
+					PHPDocLineField(n, part.Line(), 1),
+					"Void type can only be used as a standalone type for the return type",
+				)
+			}
+
+			r.checkUndefinedClassesInPHPDoc(n, returnType, part)
+			continue
+		}
+
+		// Rest is for @param handling.
+
+		if rawPart.Name() != "param" {
+			continue
+		}
+
+		part := rawPart.(*phpdoc.TypeVarCommentPart)
+		switch {
+		case part.Var == "":
+			errors.pushLint(
+				PHPDocLineField(n, part.Line(), 1),
+				"Malformed @param tag (maybe var is missing?)",
+			)
+
+		case part.Type.IsEmpty():
+			errors.pushLint(
+				PHPDocLineField(n, part.Line(), 1),
+				"Malformed @param %s tag (maybe type is missing?)", part.Var,
+			)
+
+			continue
+		}
+
+		if part.VarIsFirst {
+			// Phpstorm gives the same message.
+			errors.pushLint(
+				PHPDocLine(n, part.Line()),
+				"Non-canonical order of variable and type",
+			)
+		}
+
+		variable := part.Var
+		if !strings.HasPrefix(variable, "$") {
+			if len(actualParams) > curParam {
+				variable = actualParams[curParam].(*ir.Parameter).Variable.Name
+			}
+		}
+		if _, ok := actualParamNames[strings.TrimPrefix(variable, "$")]; !ok {
+			errors.pushLint(
+				PHPDocLineField(n, part.Line(), 2),
+				"@param for non-existing argument %s", variable,
+			)
+			continue
+		}
+
+		curParam++
+
+		converted := phpdoctypes.ToRealType(r.normalizer.ClassFQNProvider(), part.Type)
+
+		if converted.Warning != "" {
+			errors.pushType(
+				PHPDocLineField(n, part.Line(), 1),
+				converted.Warning,
+			)
+		}
+
+		var param phpdoctypes.Param
+		param.Typ = types.NewMapWithNormalization(r.normalizer, converted.Types)
+
+		if param.Typ.Contains("void") {
+			errors.pushType(
+				PHPDocLineField(n, part.Line(), 1),
+				"Void type can only be used as a standalone type for the return type",
+			)
+		}
+
+		r.checkUndefinedClassesInPHPDoc(n, param.Typ, part)
+	}
+
+	return errors
+}
+
+func (r *rootChecker) checkPHPDocRef(n ir.Node, part phpdoc.CommentPart) {
+	if !r.info.IsIndexingComplete() {
+		return
+	}
+
+	switch part.Name() {
+	case "mixin":
+		r.checkPHPDocMixinRef(n, part)
+	case "see":
+		r.checkPHPDocSeeRef(n, part)
+	}
+}
+
+func (r *rootChecker) checkPHPDocSeeRef(n ir.Node, part phpdoc.CommentPart) {
+	params := part.(*phpdoc.RawCommentPart).Params
+	if len(params) == 0 {
+		return
+	}
+
+	// @see supports a comma-separated list of refs.
+	var refs []string
+	for _, p := range params {
+		refs = append(refs, strings.TrimSuffix(p, ","))
+		if !strings.HasSuffix(p, ",") {
+			break
+		}
+	}
+
+	for _, ref := range refs {
+		// Sometimes people write references like `foo()` `foo...` `foo@`.
+		ref = strings.TrimRight(ref, "().;@")
+		if !r.isValidPHPDocRef(ref) {
+			r.walker.ReportPHPDoc(
+				PHPDocLineField(n, part.Line(), 1),
+				LevelWarning, "phpdocRef", "@see tag refers to unknown symbol %s", ref,
+			)
+		}
+	}
+}
+
+func (r *rootChecker) checkPHPDocMixinRef(n ir.Node, part phpdoc.CommentPart) {
+	rawPart, ok := part.(*phpdoc.RawCommentPart)
+	if !ok {
+		return
+	}
+
+	params := rawPart.Params
+	if len(params) == 0 {
+		return
+	}
+
+	name, ok := solver.GetClassName(r.state, &ir.Name{
+		Value: params[0],
+	})
+
+	if !ok {
+		return
+	}
+
+	if _, ok := r.info.GetClass(name); !ok {
+		r.walker.ReportPHPDoc(
+			PHPDocLineField(n, part.Line(), 1),
+			LevelWarning, "phpdocRef", "@mixin tag refers to unknown class %s", name,
+		)
+	}
+}
+
+func (r *rootChecker) checkUndefinedClassesInPHPDoc(n ir.Node, typesMap types.Map, part phpdoc.CommentPart) {
+	if !r.info.IsIndexingComplete() {
+		return
+	}
+
+	resolved := solver.ResolveTypes(r.info, r.state.CurrentClass, typesMap, solver.ResolverMap{})
+	typesMap = types.NewMapFromMap(resolved)
+
+	typesMap.Iterate(func(className string) {
+		if types.IsShape(className) {
+			shape, ok := r.info.GetClass(className)
+			if ok {
+				for _, info := range shape.Properties {
+					info.Typ.Iterate(func(typ string) {
+						if !types.IsClass(typ) {
+							return
+						}
+
+						r.checkUndefinedClass(typ, part, n)
+					})
+				}
+			}
+			return
+		}
+
+		if types.IsArray(className) {
+			arrayType := types.ArrayType(className)
+			if types.IsClass(arrayType) {
+				r.checkUndefinedClass(arrayType, part, n)
+			}
+			return
+		}
+
+		if !types.IsClass(className) {
+			return
+		}
+
+		r.checkUndefinedClass(className, part, n)
+	})
+}
+
+func (r *rootChecker) checkUndefinedClass(className string, part phpdoc.CommentPart, n ir.Node) {
+	// While there is no template support, this hack saves you unnecessary bugs.
+	if strings.HasSuffix(className, `\T`) {
+		return
+	}
+
+	_, ok := r.info.GetClassOrTrait(className)
+	if ok {
+		return
+	}
+	partNum := 1
+	if varPart, ok := part.(*phpdoc.TypeVarCommentPart); ok && varPart.VarIsFirst {
+		partNum = 2
+	}
+
+	r.walker.ReportPHPDoc(PHPDocLineField(n, part.Line(), partNum),
+		LevelError, "undefinedClass",
+		"Class or interface named %s does not exist", className,
+	)
+}
+
+func (r *rootChecker) isValidPHPDocRef(ref string) bool {
+	// Skip:
+	// - URLs
+	// - Things that can be a filename (e.g. "foo.php")
+	// - Wildcards (e.g. "self::FOO*")
+	// - Issue references (e.g. "#1393" "BACK-103")
+	// - RFCs
+	if strings.Contains(ref, "http:") || strings.Contains(ref, "https:") {
+		return true // OK: URL?
+	}
+	if strings.Contains(ref, "RFC") {
+		return true
+	}
+	if strings.ContainsAny(ref, ".*-#") {
+		return true
+	}
+
+	// expandName tries to convert s symbol into fully qualified form.
+	expandName := func(s string) string {
+		s, ok := solver.GetClassName(r.state, &ir.Name{Value: s})
+		if !ok {
+			return s
+		}
+		return s
+	}
+
+	isValidGlobalVar := func(ref string) bool {
+		// Since we don't have an exhaustive list of globals,
+		// we can't tell for sure whether a variable ref is correct.
+		return true
+	}
+
+	isValidClassSymbol := func(ref string) bool {
+		parts := strings.Split(ref, "::")
+		if len(parts) != 2 {
+			return false
+		}
+		typeName, symbolName := expandName(parts[0]), parts[1]
+		if symbolName == "class" {
+			_, ok := r.info.GetClass(typeName)
+			return ok
+		}
+		if strings.HasPrefix(symbolName, "$") {
+			return classHasProp(r.state, typeName, symbolName)
+		}
+		if _, ok := solver.FindMethod(r.info, typeName, symbolName); ok {
+			return true
+		}
+		if _, _, ok := solver.FindConstant(r.info, typeName, symbolName); ok {
+			return true
+		}
+		return false
+	}
+
+	isValidSymbol := func(ref string) bool {
+		if !strings.HasPrefix(ref, `\`) {
+			if r.currentClassNodeStack.Current() != nil {
+				className := r.state.CurrentClass
+				if _, ok := solver.FindMethod(r.info, className, ref); ok {
+					return true // OK: class method reference
+				}
+				if _, _, ok := solver.FindConstant(r.info, className, ref); ok {
+					return true // OK: class constant reference
+				}
+				if classHasProp(r.state, className, ref) {
+					return true // OK: class prop reference
+				}
+			}
+
+			// Functions and constants fall back in global namespace resolving.
+			// See https://www.php.net/manual/en/language.namespaces.fallback.php
+			globalRef := `\` + ref
+			if _, ok := r.info.GetFunction(globalRef); ok {
+				return true // OK: function reference
+			}
+			if _, ok := r.info.GetConstant(globalRef); ok {
+				return true // OK: const reference
+			}
+		}
+		fqnRef := expandName(ref)
+		if _, ok := r.info.GetFunction(fqnRef); ok {
+			return true // OK: FQN function reference
+		}
+		if _, ok := r.info.GetClass(fqnRef); ok {
+			return true // OK: FQN class reference
+		}
+		if _, ok := r.info.GetConstant(fqnRef); ok {
+			return true // OK: FQN const reference
+		}
+		return false
+	}
+
+	switch {
+	case strings.Contains(ref, "::"):
+		return isValidClassSymbol(ref)
+	case strings.HasPrefix(ref, "$"):
+		return isValidGlobalVar(ref)
+	default:
+		return isValidSymbol(ref)
+	}
+}
+
+func (r *rootChecker) CheckNameCase(n ir.Node, nameUsed, nameExpected string) {
+	if nameUsed == "" || nameExpected == "" {
+		return
+	}
+	if nameUsed != nameExpected {
+		r.walker.Report(n, LevelWarning, "nameMismatch", "%s should be spelled %s",
+			nameUsed, nameExpected)
+	}
+}
+
+func (r *rootChecker) CheckKeywordCase(n ir.Node, keyword string) {
+	toks := irutil.Keywords(n)
+	if toks == nil {
+		return
+	}
+
+	tok := toks[0]
+
+	switch n := n.(type) {
+	case *ir.YieldFromExpr:
+		r.compareKeywordWithTokenCase(n, toks[0], "yield")
+		r.compareKeywordWithTokenCase(n, toks[1], "from")
+
+	case *ir.ElseIfStmt:
+		if !n.Merged {
+			r.compareKeywordWithTokenCase(n, toks[0], "if")
+			r.compareKeywordWithTokenCase(n, toks[1], "else")
+		} else {
+			r.compareKeywordWithTokenCase(n, tok, "elseif")
+		}
+
+	default:
+		r.compareKeywordWithTokenCase(n, tok, keyword)
+	}
+}
+
+func (r *rootChecker) compareKeywordWithTokenCase(n ir.Node, tok *token.Token, keyword string) {
+	wantKwd := keyword
+	haveKwd := tok.Value
+	if wantKwd != string(haveKwd) {
+		r.walker.Report(n, LevelWarning, "keywordCase", "Use %s instead of %s",
+			wantKwd, haveKwd)
+	}
+}
+
+func (r *rootChecker) CheckTypeHintNode(n ir.Node, place string) {
+	if !r.info.IsIndexingComplete() || n == nil {
+		return
+	}
+
+	// We need to check this part without normalization, since
+	// otherwise parent will be replaced with the class name.
+	typeList := types.TypeHintTypes(n)
+	for _, typ := range typeList {
+		if typ.Elem == "parent" && r.state.CurrentClass != "" {
+			if r.state.CurrentParentClass == "" {
+				r.walker.Report(n, LevelError, "typeHint", "Cannot use 'parent' typehint when current class has no parent")
+			}
+		}
+	}
+
+	_, inTrait := r.currentClassNodeStack.Current().(*ir.TraitStmt)
+
+	typesMap := types.NewMapWithNormalization(r.normalizer, typeList)
+
+	typesMap.Iterate(func(typ string) {
+		if types.IsClass(typ) {
+			className := typ
+
+			_, hasTrait := r.info.GetTrait(className)
+			if hasTrait && !inTrait {
+				r.walker.Report(n, LevelWarning, "badTraitUse", "Cannot use trait %s as a typehint for %s", strings.TrimPrefix(className, `\`), place)
+			}
+
+			class, hasClass := r.info.GetClass(className)
+
+			if !hasClass && !hasTrait {
+				r.walker.Report(n, LevelError, "undefinedClass",
+					"Class or interface named %s does not exist", className,
+				)
+			}
+
+			r.CheckNameCase(n, className, class.Name)
+		}
+	})
+}
+
+func (r *rootChecker) CheckFuncParams(funcName *ir.Identifier, params []ir.Node, funcParams parseFuncParamsResult, phpDocParamTypes phpdoctypes.ParamsMap) {
+	for _, param := range params {
+		r.checkFuncParam(param.(*ir.Parameter))
+	}
+
+	r.checkParamsTypeHint(funcName, funcParams, phpDocParamTypes)
+}
+
+func (r *rootChecker) checkFuncParam(p *ir.Parameter) {
+	r.CheckVarNameMisspellings(p, p.Variable.Name)
+
+	// TODO(quasilyte): DefaultValue can only contain constant expressions.
+	// Could run special check over them to detect the potential fatal errors.
+	irutil.Inspect(p.DefaultValue, func(w ir.Node) bool {
+		if n, ok := w.(*ir.ArrayExpr); ok && !n.ShortSyntax {
+			r.walker.Report(n, LevelNotice, "arraySyntax", "Use the short form '[]' instead of the old 'array()'")
+
+			r.walker.addQuickFix("arraySyntax", r.quickfix.Array(n))
+		}
+		return true
+	})
+
+	r.CheckTypeHintFunctionParam(p)
+}
+
+func (r *rootChecker) CheckTypeHintFunctionParam(p *ir.Parameter) {
+	if !r.info.IsIndexingComplete() {
+		return
+	}
+
+	r.CheckTypeHintNode(p.VariableType, "parameter type")
+}
+
+func (r *rootChecker) 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
+		}
+
+		if !types.TypeHintHasMoreAccurateType(typeHintType, phpDocType) {
+			r.walker.Report(funcName, LevelNotice, "typeHint", "Specify the type for the parameter $%s in PHPDoc, 'array' type hint too generic", param)
+		}
+	}
+}
+
+func (r *rootChecker) CheckFuncReturnType(fun ir.Node, funcName string, returnTypeHint, phpDocReturnType types.Map) {
+	if !types.TypeHintHasMoreAccurateType(returnTypeHint, phpDocReturnType) {
+		r.walker.Report(fun, LevelNotice, "typeHint", "Specify the return type for the function %s in PHPDoc, 'array' type hint too generic", funcName)
+	}
+}
+
+func (r *rootChecker) CheckCommentMisspellings(n ir.Node, s string) {
+	// Try to avoid checking for symbol names and references.
+	r.checkMisspellings(n, s, "misspellComment", utils.IsCapitalized)
+}
+
+func (r *rootChecker) CheckVarNameMisspellings(n ir.Node, s string) {
+	r.checkMisspellings(n, s, "misspellName", func(string) bool {
+		return false
+	})
+}
+
+func (r *rootChecker) CheckIdentMisspellings(n *ir.Identifier) {
+	// Before PHP got context-sensitive lexer, it was common to use
+	// method names to avoid parsing errors.
+	// We can't suggest a fix that leads to a parsing error.
+	// To avoid false positives, skip PHP keywords.
+	r.checkMisspellings(n, n.Value, "misspellName", utils.IsPHPKeyword)
+}
+
+func (r *rootChecker) checkMisspellings(n ir.Node, s string, label string, skip func(string) bool) {
+	if !r.info.IsIndexingComplete() {
+		return
+	}
+	if r.typoFixer == nil {
+		return
+	}
+	_, changes := r.typoFixer.Replace(s)
+	for _, c := range changes {
+		if skip(c.Corrected) || skip(c.Original) {
+			continue
+		}
+		r.walker.Report(n, LevelNotice, label, `"%s" is a misspelling of "%s"`, c.Original, c.Corrected)
+	}
+}
+
+func (r *rootChecker) CheckAssignNullToNotNullableProperty(prop *ir.PropertyStmt, propTypes types.Map) {
+	assignNull := false
+
+	if expr, ok := prop.Expr.(*ir.ConstFetchExpr); ok {
+		assignNull = strings.EqualFold(expr.Constant.Value, "null")
+	}
+
+	if assignNull && !propTypes.Empty() {
+		onlyClasses := true
+		nullable := propTypes.Find(func(typ string) bool {
+			if !types.IsClass(typ) && typ != "null" {
+				onlyClasses = false
+			}
+			return typ == "null"
+		})
+
+		if !nullable && onlyClasses {
+			r.walker.Report(prop, LevelNotice, "propNullDefault", "Assigning null to a not nullable property")
+			r.walker.addQuickFix("propNullDefault", r.quickfix.NullForNotNullableProperty(prop))
+		}
+	}
+}
+
+func (r *rootChecker) CheckModifierKeywordCase(m *ir.Identifier) {
+	lcase := strings.ToLower(m.Value)
+	if lcase != m.Value {
+		r.walker.Report(m, LevelWarning, "keywordCase", "Use %s instead of %s",
+			lcase, m.Value)
+	}
+}
+
+func (r *rootChecker) CheckOldStyleConstructor(meth *ir.ClassMethodStmt) {
+	lastDelim := strings.LastIndexByte(r.state.CurrentClass, '\\')
+	methodName := meth.MethodName.Value
+	className := r.state.CurrentClass[lastDelim+1:]
+
+	if !strings.EqualFold(className, methodName) {
+		return
+	}
+
+	_, inClass := r.currentClassNodeStack.Current().(*ir.ClassStmt)
+	if !inClass {
+		return
+	}
+
+	r.walker.Report(meth.MethodName, LevelNotice, "oldStyleConstructor", "Old-style constructor usage, use __construct instead")
+}
+
+func (r *rootChecker) CheckPHPDocVar(n ir.Node, doc phpdoc.Comment, typ types.Map) {
+	if phpdoc.IsSuspicious([]byte(doc.Raw)) {
+		r.walker.ReportPHPDoc(PHPDocLine(n, 1),
+			LevelWarning, "phpdocLint",
+			"Multiline PHPDoc comment should start with /**, not /*",
+		)
+	}
+
+	for _, part := range doc.Parsed {
+		r.checkPHPDocRef(n, part)
+		part, ok := part.(*phpdoc.TypeVarCommentPart)
+		if ok && part.Name() == "var" {
+			converted := phpdoctypes.ToRealType(r.normalizer.ClassFQNProvider(), part.Type)
+
+			if converted.Warning != "" {
+				field := 1
+				if part.VarIsFirst {
+					field = 2
+				}
+				r.walker.ReportPHPDoc(PHPDocLineField(n, part.Line(), field),
+					LevelNotice, "phpdocType",
+					converted.Warning,
+				)
+			}
+
+			r.checkUndefinedClassesInPHPDoc(n, typ, part)
+		}
+	}
+}
+
+func (r *rootChecker) CheckParentConstructorCall(n ir.Node, parentConstructorCalled bool) {
+	if !r.info.IsIndexingComplete() {
+		return
+	}
+
+	class, ok := r.currentClassNodeStack.Current().(*ir.ClassStmt)
+	if !ok || class.Extends == nil {
+		return
+	}
+	m, ok := solver.FindMethod(r.info, r.state.CurrentParentClass, `__construct`)
+	if !ok || m.Info.AccessLevel == meta.Private || m.Info.IsAbstract() {
+		return
+	}
+
+	if !parentConstructorCalled {
+		r.walker.Report(n, LevelWarning, "parentConstructor", "Missing parent::__construct() call")
+	}
+}
+
+func (r *rootChecker) CheckMagicMethod(meth ir.Node, name string, modif methodModifiers, countArgs int) {
+	const Any = -1
+	var (
+		canBeStatic    bool
+		canBeNonPublic bool
+		mustBeStatic   bool
+
+		numArgsExpected int
+	)
+
+	switch name {
+	case "__call",
+		"__set":
+		canBeStatic = false
+		canBeNonPublic = false
+		numArgsExpected = 2
+
+	case "__get",
+		"__isset",
+		"__unset":
+		canBeStatic = false
+		canBeNonPublic = false
+		numArgsExpected = 1
+
+	case "__toString":
+		canBeStatic = false
+		canBeNonPublic = false
+		numArgsExpected = 0
+
+	case "__invoke",
+		"__debugInfo":
+		canBeStatic = false
+		canBeNonPublic = false
+		numArgsExpected = Any
+
+	case "__construct":
+		canBeStatic = false
+		canBeNonPublic = true
+		numArgsExpected = Any
+
+	case "__destruct", "__clone":
+		canBeStatic = false
+		canBeNonPublic = true
+		numArgsExpected = 0
+
+	case "__callStatic":
+		canBeStatic = true
+		canBeNonPublic = false
+		mustBeStatic = true
+		numArgsExpected = 2
+
+	case "__sleep",
+		"__wakeup",
+		"__serialize",
+		"__unserialize",
+		"__set_state":
+		canBeNonPublic = true
+		canBeStatic = true
+		numArgsExpected = Any
+
+	default:
+		return // Not a magic method
+	}
+
+	switch {
+	case mustBeStatic && !modif.static:
+		r.walker.Report(meth, LevelError, "magicMethodDecl", "The magic method %s() must be static", name)
+	case !canBeStatic && modif.static:
+		r.walker.Report(meth, LevelError, "magicMethodDecl", "The magic method %s() cannot be static", name)
+	}
+	if !canBeNonPublic && modif.accessLevel != meta.Public {
+		r.walker.Report(meth, LevelError, "magicMethodDecl", "The magic method %s() must have public visibility", name)
+	}
+
+	if countArgs != numArgsExpected && numArgsExpected != Any {
+		r.walker.Report(meth, LevelError, "magicMethodDecl", "The magic method %s() must take exactly %d argument", name, numArgsExpected)
+	}
+}
+
+func (r *rootChecker) CheckExtends(class ir.Node, currentClass meta.ClassInfo, extends *ir.ClassExtendsStmt) {
+	if extends == nil {
+		return
+	}
+
+	r.CheckKeywordCase(extends, "extends")
+
+	className, ok := solver.GetClassName(r.state, extends.ClassName)
+	if ok {
+		r.CheckClassInherit(class, extends.ClassName, currentClass, className)
+	}
+}
+
+func (r *rootChecker) CheckImplements(class ir.Node, currentClass meta.ClassInfo, implements *ir.ClassImplementsStmt) {
+	if implements == nil {
+		return
+	}
+
+	r.CheckKeywordCase(implements, "implements")
+
+	for _, tr := range implements.InterfaceNames {
+		interfaceName, ok := solver.GetClassName(r.state, tr)
+		if ok {
+			currentClass.Interfaces[interfaceName] = struct{}{}
+			r.CheckInterfaceImplemented(class, tr, currentClass, interfaceName)
+		}
+	}
+}
+
+func (r *rootChecker) CheckTraitImplemented(classNode, name ir.Node, currentClass meta.ClassInfo, nameUsed string) {
+	if !r.info.IsIndexingComplete() {
+		return
+	}
+	trait, ok := r.info.GetTrait(nameUsed)
+	if !ok {
+		r.ReportUndefinedTrait(name, nameUsed)
+		return
+	}
+	r.checkImplemented(classNode, name, nameUsed, currentClass, trait)
+}
+
+func (r *rootChecker) CheckClassInherit(classNode, extendsClassNameNode ir.Node, currentClass meta.ClassInfo, nameUsed string) {
+	if !r.info.IsIndexingComplete() {
+		return
+	}
+
+	class, ok := r.info.GetClass(nameUsed)
+	if !ok {
+		r.ReportUndefinedClass(extendsClassNameNode, nameUsed)
+		return
+	}
+
+	r.checkClassExtends(extendsClassNameNode, currentClass, class)
+	r.checkImplemented(classNode, extendsClassNameNode, nameUsed, currentClass, class)
+}
+
+func (r *rootChecker) checkClassExtends(extendsClassNameNode ir.Node, currentClass, otherClass meta.ClassInfo) {
+	if otherClass.IsFinal() {
+		r.walker.Report(extendsClassNameNode, LevelError, "invalidExtendClass", "Class %s may not inherit from final class %s", currentClass.Name, otherClass.Name)
+	}
+}
+
+func (r *rootChecker) checkClassImplemented(classNode, extendsClassNameNode ir.Node, currentClass meta.ClassInfo, nameUsed string) {
+	if !r.info.IsIndexingComplete() {
+		return
+	}
+
+	class, ok := r.info.GetClass(nameUsed)
+	if !ok {
+		r.ReportUndefinedClass(extendsClassNameNode, nameUsed)
+		return
+	}
+
+	r.checkImplemented(classNode, extendsClassNameNode, nameUsed, currentClass, class)
+}
+
+func (r *rootChecker) CheckInterfaceImplemented(classNode, name ir.Node, currentClass meta.ClassInfo, nameUsed string) {
+	r.checkClassImplemented(classNode, name, currentClass, nameUsed)
+}
+
+func (r *rootChecker) checkImplemented(classNode, name ir.Node, nameUsed string, currentClass, otherClass meta.ClassInfo) {
+	if r.state.IsTrait || currentClass.IsAbstract() {
+		return
+	}
+
+	r.CheckNameCase(name, nameUsed, otherClass.Name)
+	visited := make(map[string]struct{}, 4)
+	r.checkImplementedStep(classNode, name, nameUsed, otherClass, visited)
+}
+
+func (r *rootChecker) checkImplementedStep(classNode, name ir.Node, className string, otherClass meta.ClassInfo, visited map[string]struct{}) {
+	// TODO: check that method signatures are compatible?
+	if _, ok := visited[className]; ok {
+		return
+	}
+	visited[className] = struct{}{}
+
+	for _, ifaceMethod := range otherClass.Methods.H {
+		m, ok := solver.FindMethod(r.info, r.state.CurrentClass, ifaceMethod.Name)
+		if !ok || !m.Implemented {
+			r.walker.Report(name, LevelError, "unimplemented", "Class %s must implement %s::%s method",
+				r.state.CurrentClass, className, ifaceMethod.Name)
+			continue
+		}
+		if m.Info.Name != ifaceMethod.Name {
+			r.walker.Report(name, LevelNotice, "nameMismatch", "%s::%s should be spelled as %s::%s",
+				r.state.CurrentClass, m.Info.Name, className, ifaceMethod.Name)
+
+		}
+		if ifaceMethod.IsFinal() && ifaceMethod.AccessLevel != meta.Private {
+			methodNode := irutil.FindClassMethodNode(classNode, ifaceMethod.Name)
+			if methodNode != nil {
+				r.walker.Report(methodNode, LevelError, "methodSignatureMismatch",
+					"Method %s::%s is declared final and cannot be overridden",
+					otherClass.Name, ifaceMethod.Name)
+			}
+		}
+	}
+
+	for _, ifaceName := range otherClass.ParentInterfaces {
+		iface, ok := r.info.GetClass(ifaceName)
+		if ok {
+			r.checkImplementedStep(classNode, name, ifaceName, iface, visited)
+		}
+	}
+
+	if otherClass.Parent != "" {
+		class, ok := r.info.GetClass(otherClass.Parent)
+		if ok {
+			r.checkImplementedStep(classNode, name, otherClass.Parent, class, visited)
+		}
+	}
+}
+
+func (r *rootChecker) ReportUndefinedClass(n ir.Node, name string) {
+	r.walker.Report(n, LevelError, "undefinedClass", "Class or interface named %s does not exist", name)
+}
+
+func (r *rootChecker) ReportUndefinedTrait(n ir.Node, name string) {
+	r.walker.Report(n, LevelError, "undefinedTrait", "Trait named %s does not exist", name)
+}
diff --git a/src/linter/worker.go b/src/linter/worker.go
index 7207b55..8bdaf38 100644
--- a/src/linter/worker.go
+++ b/src/linter/worker.go
@@ -294,6 +294,8 @@ func (w *Worker) analyzeFile(file *workspace.File, rootNode *ir.Root) (*rootWalk
 		checkersFilter:      w.checkersFilter,
 	}
 
+	walker.checker = newRootChecker(walker, NewQuickFixGenerator(file))
+
 	walker.InitCustom()
 
 	walker.beforeEnterFile()
diff --git a/src/types/typehint.go b/src/types/typehint.go
index 48dec61..0beec4f 100644
--- a/src/types/typehint.go
+++ b/src/types/typehint.go
@@ -10,6 +10,20 @@ func NormalizedTypeHintTypes(normalizer Normalizer, typeNode ir.Node) Map {
 	return NewMapWithNormalization(normalizer, typeList)
 }
 
+func TypeHintHasMoreAccurateType(typeHintType, phpDocType Map) bool {
+	// If is not array typehint.
+	if !typeHintType.IsLazyArrayOf("mixed") {
+		return true
+	}
+
+	// If it has more accurate type.
+	if !phpDocType.Empty() {
+		return true
+	}
+
+	return false
+}
+
 // TypeHintTypes converts type hint node to meta types.
 //
 // No normalization is performed.


Test patch:
diff --git a/src/linter/phpdoc_test.go b/src/linter/phpdoc_test.go
index 0e33099..eea9565 100644
--- a/src/linter/phpdoc_test.go
+++ b/src/linter/phpdoc_test.go
@@ -8,6 +8,7 @@ import (
 	"github.com/VKCOM/noverify/src/phpdoc"
 	"github.com/VKCOM/noverify/src/phpdoctypes"
 	"github.com/VKCOM/noverify/src/types"
+	"github.com/VKCOM/noverify/src/workspace"
 )
 
 func TestParseClassPHPDoc(t *testing.T) {
@@ -62,7 +63,10 @@ func TestParseClassPHPDoc(t *testing.T) {
 
 	l := NewLinter(NewConfig("8.1"))
 	st := &meta.ClassParseState{Info: l.MetaInfo()}
-	walker := rootWalker{ctx: newRootContext(l.config, NewWorkerContext(), st)}
+	walker := rootWalker{
+		ctx: newRootContext(l.config, NewWorkerContext(), st),
+	}
+	walker.checker = newRootChecker(&walker, NewQuickFixGenerator(workspace.NewFile("test.php", []byte{})))
 	for _, test := range tests {
 		docString := fmt.Sprintf(`/** %s */`, test.line)
 		doc := phpdoc.Parse(walker.ctx.phpdocTypeParser, docString)