Repository: VKCOM/noverify
Pull request #839 (2020-12-07 20:02:06)

move some block_walkers functions to block_linter


Patch:
diff --git a/src/linter/block.go b/src/linter/block.go
index 4aab623..c475498 100644
--- a/src/linter/block.go
+++ b/src/linter/block.go
@@ -2,7 +2,6 @@ package linter
 
 import (
 	"fmt"
-	"math"
 	"strings"
 
 	"github.com/VKCOM/noverify/src/ir"
@@ -188,11 +187,8 @@ func (b *BlockWalker) EnterNode(n ir.Node) (res bool) {
 	case *ir.LogicalOrExpr:
 		res = b.handleLogicalOr(s)
 
-	case *ir.ArrayDimFetchExpr:
-		b.checkArrayDimFetch(s)
-
 	case *ir.GlobalStmt:
-		b.checkGlobalStmt(s)
+		b.handleAndCheckGlobalStmt(s)
 		res = false
 	case *ir.StaticStmt:
 		for _, vv := range s.Vars {
@@ -364,7 +360,7 @@ func (b *BlockWalker) checkDupGlobal(s *ir.GlobalStmt) {
 	}
 }
 
-func (b *BlockWalker) checkGlobalStmt(s *ir.GlobalStmt) {
+func (b *BlockWalker) handleAndCheckGlobalStmt(s *ir.GlobalStmt) {
 	if !b.rootLevel {
 		b.checkDupGlobal(s)
 	}
@@ -669,7 +665,7 @@ func (b *BlockWalker) handleCatch(s *ir.CatchStmt) {
 
 // We still need to analyze expressions in isset()/unset()/empty() statements
 func (b *BlockWalker) handleIssetDimFetch(e *ir.ArrayDimFetchExpr) {
-	b.checkArrayDimFetch(e)
+	b.linter.checkArrayDimFetch(e)
 
 	switch v := e.Variable.(type) {
 	case *ir.SimpleVar:
@@ -687,50 +683,7 @@ func (b *BlockWalker) handleIssetDimFetch(e *ir.ArrayDimFetchExpr) {
 	}
 }
 
-func (b *BlockWalker) checkArrayDimFetch(s *ir.ArrayDimFetchExpr) {
-	if !meta.IsIndexingComplete() {
-		return
-	}
-
-	typ := solver.ExprType(b.ctx.sc, b.r.ctx.st, s.Variable)
-
-	var (
-		maybeHaveClasses bool
-		haveArrayAccess  bool
-	)
-
-	typ.Iterate(func(t string) {
-		// FullyQualified class name will have "\" in the beginning
-		if meta.IsClassType(t) {
-			maybeHaveClasses = true
-
-			if !haveArrayAccess && solver.Implements(t, `\ArrayAccess`) {
-				haveArrayAccess = true
-			}
-		}
-	})
-
-	if maybeHaveClasses && !haveArrayAccess {
-		b.r.Report(s.Variable, LevelDoNotReject, "arrayAccess", "Array access to non-array type %s", typ)
-	}
-}
-
-func (b *BlockWalker) handleArgsCount(n ir.Node, args []ir.Node, fn meta.FuncInfo) {
-	if meta.NameNodeEquals(n, "mt_rand") {
-		if len(args) != 0 && len(args) != 2 {
-			b.r.Report(n, LevelWarning, "argCount", "mt_rand expects 0 or 2 args")
-		}
-		return
-	}
-
-	if !enoughArgs(args, fn) {
-		b.r.Report(n, LevelWarning, "argCount", "Too few arguments for %s", meta.NameNodeToString(n))
-	}
-}
-
-func (b *BlockWalker) handleCallArgs(n ir.Node, args []ir.Node, fn meta.FuncInfo) {
-	b.handleArgsCount(n, args, fn)
-
+func (b *BlockWalker) handleCallArgs(args []ir.Node, fn meta.FuncInfo) {
 	for i, arg := range args {
 		if i >= len(fn.Params) {
 			arg.Walk(b)
@@ -750,7 +703,7 @@ func (b *BlockWalker) handleCallArgs(n ir.Node, args []ir.Node, fn meta.FuncInfo
 			a.Walk(b)
 		case *ir.ArrayDimFetchExpr:
 			if ref {
-				b.handleDimFetchLValue(a, "call_with_ref", meta.MixedType)
+				b.handleAndCheckDimFetchLValue(a, "call_with_ref", meta.MixedType)
 				break
 			}
 			a.Walk(b)
@@ -783,38 +736,17 @@ func (b *BlockWalker) handleCallArgs(n ir.Node, args []ir.Node, fn meta.FuncInfo
 func (b *BlockWalker) handleFunctionCall(e *ir.FunctionCallExpr) bool {
 	call := resolveFunctionCall(b.ctx.sc, b.r.ctx.st, b.ctx.customTypes, e)
 
-	if meta.IsIndexingComplete() {
-		if !call.canAnalyze {
-			return true
-		}
-
-		if !call.defined && !b.ctx.customFunctionExists(e.Function) {
-			b.r.Report(e.Function, LevelError, "undefined", "Call to undefined function %s", meta.NameNodeToString(e.Function))
-		}
-		b.r.checkNameCase(e.Function, call.fqName, call.info.Name)
-	}
-
-	if call.info.Doc.Deprecated {
-		if call.info.Doc.DeprecationNote != "" {
-			b.r.Report(e.Function, LevelDoNotReject, "deprecated", "Call to deprecated function %s (%s)",
-				meta.NameNodeToString(e.Function), call.info.Doc.DeprecationNote)
-		} else {
-			b.r.Report(e.Function, LevelDoNotReject, "deprecated", "Call to deprecated function %s",
-				meta.NameNodeToString(e.Function))
-		}
-	}
-
 	e.Function.Walk(b)
 
-	if call.fqName == `\func_get_args` {
+	switch call.funcName {
+	case `\func_get_args`:
 		b.callsFuncGetArgs = true
-	}
-
-	if call.fqName == `\compact` {
+	case `\compact`:
 		b.handleCompactCallArgs(e.Args)
-	} else {
-		b.handleCallArgs(e.Function, e.Args, call.info)
+	default:
+		b.handleCallArgs(e.Args, call.info)
 	}
+
 	b.ctx.exitFlags |= call.info.ExitFlags
 
 	return false
@@ -852,129 +784,28 @@ func (b *BlockWalker) handleCompactCallArgs(args []ir.Node) {
 }
 
 func (b *BlockWalker) handleMethodCall(e *ir.MethodCallExpr) bool {
-	if !meta.IsIndexingComplete() {
-		return true
-	}
-
-	var methodName string
-
-	switch id := e.Method.(type) {
-	case *ir.Identifier:
-		methodName = id.Value
-	default:
-		return true
-	}
-
-	var (
-		matchDist   = int(math.MaxInt32)
-		foundMethod bool
-		magic       bool
-		fn          meta.FuncInfo
-		className   string
-	)
-
-	exprType := b.exprType(e.Variable)
-
-	exprType.Find(func(typ string) bool {
-		m, isMagic, ok := findMethod(typ, methodName)
-		if !ok {
-			return false
-		}
-		foundMethod = true
-		if dist := classDistance(b.r.ctx.st, typ); dist < matchDist {
-			matchDist = dist
-			fn = m.Info
-			className = m.ClassName
-			magic = isMagic
-		}
-		return matchDist == 0 // Stop if found inside the current class
-	})
+	call := resolveMethodCall(b.ctx.sc, b.r.ctx.st, b.ctx.customTypes, e)
 
 	e.Variable.Walk(b)
 	e.Method.Walk(b)
 
-	if !foundMethod && !magic && !b.r.ctx.st.IsTrait && !b.isThisInsideClosure(e.Variable) {
-		// The method is undefined but we permit calling it if `method_exists`
-		// was called prior to that call.
-		if !b.ctx.customMethodExists(e.Variable, methodName) {
-			b.r.Report(e.Method, LevelError, "undefined", "Call to undefined method {%s}->%s()", exprType, methodName)
-		}
-	} else if !magic && !b.r.ctx.st.IsTrait {
-		// Method is defined.
-		b.r.checkNameCase(e.Method, methodName, fn.Name)
-		if fn.IsStatic() {
-			b.r.Report(e.Method, LevelWarning, "callStatic", "Calling static method as instance method")
-		}
-	}
-
-	if fn.Doc.Deprecated {
-		if fn.Doc.DeprecationNote != "" {
-			b.r.Report(e.Method, LevelDoNotReject, "deprecated", "Call to deprecated method {%s}->%s() (%s)",
-				exprType, methodName, fn.Doc.DeprecationNote)
-		} else {
-			b.r.Report(e.Method, LevelDoNotReject, "deprecated", "Call to deprecated method {%s}->%s()",
-				exprType, methodName)
-		}
+	if !call.isMagic {
+		b.handleCallArgs(e.Args, call.info)
 	}
-
-	if foundMethod && !magic && !canAccess(b.r.ctx.st, className, fn.AccessLevel) {
-		b.r.Report(e.Method, LevelError, "accessLevel", "Cannot access %s method %s->%s()", fn.AccessLevel, className, methodName)
-	}
-
-	if !magic {
-		b.handleCallArgs(e.Method, e.Args, fn)
-	}
-	b.ctx.exitFlags |= fn.ExitFlags
+	b.ctx.exitFlags |= call.info.ExitFlags
 
 	return false
 }
 
 func (b *BlockWalker) handleStaticCall(e *ir.StaticCallExpr) bool {
-	if !meta.IsIndexingComplete() {
-		return true
-	}
-
-	var methodName string
-
-	switch id := e.Call.(type) {
-	case *ir.Identifier:
-		methodName = id.Value
-	default:
-		return true
-	}
-
-	classNameNode, ok := e.Class.(*ir.Name)
-	parentCall := ok && classNameNode.Value == "parent"
-	if parentCall && methodName == "__construct" {
-		b.callsParentConstructor = true
-	}
-
-	className, ok := solver.GetClassName(b.r.ctx.st, e.Class)
-	if !ok {
-		return true
-	}
-
-	m, ok := solver.FindMethod(className, methodName)
-	fn := m.Info
+	call := resolveStaticMethodCall(b.r.ctx.st, e)
+	b.callsParentConstructor = call.isCallsParentConstructor
 
 	e.Class.Walk(b)
 	e.Call.Walk(b)
 
-	magic := haveMagicMethod(className, `__callStatic`)
-	if !ok && !magic && !b.r.ctx.st.IsTrait {
-		b.r.Report(e.Call, LevelError, "undefined", "Call to undefined method %s::%s()", className, methodName)
-	} else if !parentCall && !fn.IsStatic() && !magic && !b.r.ctx.st.IsTrait {
-		// Method is defined.
-		// parent::f() is permitted.
-		b.r.Report(e.Call, LevelWarning, "callStatic", "Calling instance method as static method")
-	}
-
-	if ok && !canAccess(b.r.ctx.st, m.ClassName, fn.AccessLevel) {
-		b.r.Report(e.Call, LevelError, "accessLevel", "Cannot access %s method %s::%s()", fn.AccessLevel, m.ClassName, methodName)
-	}
-
-	b.handleCallArgs(e.Call, e.Args, fn)
-	b.ctx.exitFlags |= fn.ExitFlags
+	b.handleCallArgs(e.Args, call.methodInfo.Info)
+	b.ctx.exitFlags |= call.methodInfo.Info.ExitFlags
 
 	return false
 }
@@ -994,89 +825,24 @@ func (b *BlockWalker) isThisInsideClosure(varNode ir.Node) bool {
 func (b *BlockWalker) handlePropertyFetch(e *ir.PropertyFetchExpr) bool {
 	e.Variable.Walk(b)
 	e.Property.Walk(b)
-
-	if !meta.IsIndexingComplete() {
-		return false
-	}
-
-	id, ok := e.Property.(*ir.Identifier)
-	if !ok {
-		return false
-	}
-
-	found := false
-	magic := false
-	matchDist := int(math.MaxInt32)
-	var className string
-	var info meta.PropertyInfo
-
-	typ := b.exprType(e.Variable)
-	typ.Find(func(typ string) bool {
-		p, isMagic, ok := findProperty(typ, id.Value)
-		if !ok {
-			return false
-		}
-		found = true
-		if dist := classDistance(b.r.ctx.st, typ); dist < matchDist {
-			matchDist = dist
-			info = p.Info
-			className = p.ClassName
-			magic = isMagic
-		}
-		return matchDist == 0 // Stop if found inside the current class
-	})
-
-	if !found && !magic && !b.r.ctx.st.IsTrait && !b.isThisInsideClosure(e.Variable) {
-		b.r.Report(e.Property, LevelError, "undefined", "Property {%s}->%s does not exist", typ, id.Value)
-	}
-
-	if found && !magic && !canAccess(b.r.ctx.st, className, info.AccessLevel) {
-		b.r.Report(e.Property, LevelError, "accessLevel", "Cannot access %s property %s->%s", info.AccessLevel, className, id.Value)
-	}
-
 	return false
 }
 
 func (b *BlockWalker) handleStaticPropertyFetch(e *ir.StaticPropertyFetchExpr) bool {
 	e.Class.Walk(b)
 
-	if !meta.IsIndexingComplete() {
-		return false
-	}
-
-	sv, ok := e.Property.(*ir.SimpleVar)
-	if !ok {
-		vv := e.Property.(*ir.Var)
-		vv.Expr.Walk(b)
-		return false
-	}
-
-	className, ok := solver.GetClassName(b.r.ctx.st, e.Class)
-	if !ok {
-		return false
-	}
-
-	p, ok := solver.FindProperty(className, "$"+sv.Name)
-	if !ok && !b.r.ctx.st.IsTrait {
-		b.r.Report(e.Property, LevelError, "undefined", "Property %s::$%s does not exist", className, sv.Name)
-	}
-
-	if ok && !canAccess(b.r.ctx.st, p.ClassName, p.Info.AccessLevel) {
-		b.r.Report(e.Property, LevelError, "accessLevel", "Cannot access %s property %s::$%s", p.Info.AccessLevel, p.ClassName, sv.Name)
+	if propertyVarNode, propertyIsVarNode := e.Property.(*ir.Var); propertyIsVarNode {
+		propertyVarNode.Expr.Walk(b)
 	}
 
 	return false
 }
 
 func (b *BlockWalker) handleArray(arr *ir.ArrayExpr) bool {
-	return b.handleArrayItems(arr, arr.Items)
+	return b.handleArrayItems(arr.Items)
 }
 
-func (b *BlockWalker) handleArrayItems(arr ir.Node, items []*ir.ArrayItemExpr) bool {
-	if !meta.IsIndexingComplete() {
-		return true
-	}
-
+func (b *BlockWalker) handleArrayItems(items []*ir.ArrayItemExpr) bool {
 	for _, item := range items {
 		if item.Val != nil {
 			item.Val.Walk(b)
@@ -1090,32 +856,7 @@ func (b *BlockWalker) handleArrayItems(arr ir.Node, items []*ir.ArrayItemExpr) b
 }
 
 func (b *BlockWalker) handleClassConstFetch(e *ir.ClassConstFetchExpr) bool {
-	if !meta.IsIndexingComplete() {
-		return true
-	}
-
-	constName := e.ConstantName
-	if constName.Value == `class` || constName.Value == `CLASS` {
-		return false
-	}
-
-	className, ok := solver.GetClassName(b.r.ctx.st, e.Class)
-	if !ok {
-		return false
-	}
-
-	info, implClass, ok := solver.FindConstant(className, constName.Value)
-
 	e.Class.Walk(b)
-
-	if !ok && !b.r.ctx.st.IsTrait {
-		b.r.Report(e.ConstantName, LevelError, "undefined", "Class constant %s::%s does not exist", className, constName.Value)
-	}
-
-	if ok && !canAccess(b.r.ctx.st, implClass, info.AccessLevel) {
-		b.r.Report(e.ConstantName, LevelError, "accessLevel", "Cannot access %s constant %s::%s", info.AccessLevel, implClass, constName.Value)
-	}
-
 	return false
 }
 
@@ -1248,6 +989,8 @@ func (b *BlockWalker) enterClosure(fun *ir.ClosureExpr, haveThis bool, thisType
 			byRef = true
 		case *ir.SimpleVar:
 			v = u
+		default:
+			continue
 		}
 
 		if !b.ctx.sc.HaveVar(v) && !byRef {
@@ -1685,8 +1428,8 @@ func (b *BlockWalker) handleSwitch(s *ir.SwitchStmt) bool {
 // if $a was previously undefined,
 // handle case when doing assignment like '$a[] = 4;'
 // or call to function that accepts like exec("command", $a)
-func (b *BlockWalker) handleDimFetchLValue(e *ir.ArrayDimFetchExpr, reason string, typ meta.TypesMap) {
-	b.checkArrayDimFetch(e)
+func (b *BlockWalker) handleAndCheckDimFetchLValue(e *ir.ArrayDimFetchExpr, reason string, typ meta.TypesMap) {
+	b.linter.checkArrayDimFetch(e)
 
 	switch v := e.Variable.(type) {
 	case *ir.Var, *ir.SimpleVar:
@@ -1697,7 +1440,7 @@ func (b *BlockWalker) handleDimFetchLValue(e *ir.ArrayDimFetchExpr, reason strin
 		b.addVar(v, arrTyp, reason, meta.VarAlwaysDefined)
 		b.handleVariable(v)
 	case *ir.ArrayDimFetchExpr:
-		b.handleDimFetchLValue(v, reason, meta.MixedType)
+		b.handleAndCheckDimFetchLValue(v, reason, meta.MixedType)
 	default:
 		// probably not assignable?
 		v.Walk(b)
@@ -1712,7 +1455,7 @@ func (b *BlockWalker) handleDimFetchLValue(e *ir.ArrayDimFetchExpr, reason strin
 func (b *BlockWalker) handleAssignReference(a *ir.AssignReference) bool {
 	switch v := a.Variable.(type) {
 	case *ir.ArrayDimFetchExpr:
-		b.handleDimFetchLValue(v, "assign_array", meta.MixedType)
+		b.handleAndCheckDimFetchLValue(v, "assign_array", meta.MixedType)
 		a.Expression.Walk(b)
 		return false
 	case *ir.Var, *ir.SimpleVar:
@@ -1828,7 +1571,7 @@ func (b *BlockWalker) handleAssign(a *ir.Assign) bool {
 	switch v := a.Variable.(type) {
 	case *ir.ArrayDimFetchExpr:
 		typ := solver.ExprTypeLocal(b.ctx.sc, b.r.ctx.st, a.Expression)
-		b.handleDimFetchLValue(v, "assign_array", typ)
+		b.handleAndCheckDimFetchLValue(v, "assign_array", typ)
 		return false
 	case *ir.SimpleVar:
 		b.paramClobberCheck(v)
diff --git a/src/linter/block_linter.go b/src/linter/block_linter.go
index 31fdeb3..03deebd 100644
--- a/src/linter/block_linter.go
+++ b/src/linter/block_linter.go
@@ -26,9 +26,27 @@ func (b *blockLinter) enterNode(n ir.Node) {
 	case *ir.ArrayExpr:
 		b.checkArray(n)
 
+	case *ir.ArrayDimFetchExpr:
+		b.checkArrayDimFetch(n)
+
 	case *ir.FunctionCallExpr:
 		b.checkFunctionCall(n)
 
+	case *ir.MethodCallExpr:
+		b.checkMethodCall(n)
+
+	case *ir.StaticCallExpr:
+		b.checkStaticCall(n)
+
+	case *ir.PropertyFetchExpr:
+		b.checkPropertyFetch(n)
+
+	case *ir.StaticPropertyFetchExpr:
+		b.checkStaticPropertyFetch(n)
+
+	case *ir.ClassConstFetchExpr:
+		b.checkClassConstFetch(n)
+
 	case *ir.NewExpr:
 		b.checkNew(n)
 
@@ -173,6 +191,30 @@ func (b *blockLinter) report(n ir.Node, level int, checkName, msg string, args .
 	b.walker.r.Report(n, level, checkName, msg, args...)
 }
 
+func (b *blockLinter) checkArrayDimFetch(s *ir.ArrayDimFetchExpr) {
+	typ := solver.ExprType(b.walker.ctx.sc, b.walker.r.ctx.st, s.Variable)
+
+	var (
+		maybeHaveClasses bool
+		haveArrayAccess  bool
+	)
+
+	typ.Iterate(func(t string) {
+		// FullyQualified class name will have "\" in the beginning
+		if meta.IsClassType(t) {
+			maybeHaveClasses = true
+
+			if !haveArrayAccess && solver.Implements(t, `\ArrayAccess`) {
+				haveArrayAccess = true
+			}
+		}
+	})
+
+	if maybeHaveClasses && !haveArrayAccess {
+		b.report(s.Variable, LevelDoNotReject, "arrayAccess", "Array access to non-array type %s", typ)
+	}
+}
+
 func (b *blockLinter) checkAssign(a *ir.Assign) {
 	b.checkVoidType(a.Expression)
 }
@@ -648,12 +690,58 @@ func (b *blockLinter) checkArray(arr *ir.ArrayExpr) {
 	}
 }
 
-func (b *blockLinter) checkFunctionCall(e *ir.FunctionCallExpr) {
-	fqName, ok := solver.GetFuncName(b.walker.r.ctx.st, e.Function)
-	if !ok {
+func (b *blockLinter) checkDeprecatedFunctionCall(e *ir.FunctionCallExpr, call *funcCallInfo) {
+	if !call.info.Doc.Deprecated {
 		return
 	}
 
+	if call.info.Doc.DeprecationNote != "" {
+		b.report(e.Function, LevelDoNotReject, "deprecated", "Call to deprecated function %s (%s)", meta.NameNodeToString(e.Function), call.info.Doc.DeprecationNote)
+		return
+	}
+
+	b.report(e.Function, LevelDoNotReject, "deprecated", "Call to deprecated function %s", meta.NameNodeToString(e.Function))
+}
+
+func (b *blockLinter) checkFunctionAvailability(e *ir.FunctionCallExpr, call *funcCallInfo) {
+	if !call.isFound && !b.walker.ctx.customFunctionExists(e.Function) {
+		b.report(e.Function, LevelError, "undefined", "Call to undefined function %s", meta.NameNodeToString(e.Function))
+	}
+}
+
+func (b *blockLinter) checkCallArgs(n ir.Node, args []ir.Node, fn meta.FuncInfo) {
+	b.checkCallArgsCount(n, args, fn)
+}
+
+func (b *blockLinter) checkCallArgsCount(n ir.Node, args []ir.Node, fn meta.FuncInfo) {
+	if fn.Name == `\mt_rand` {
+		if len(args) != 0 && len(args) != 2 {
+			b.report(n, LevelWarning, "argCount", "mt_rand expects 0 or 2 args")
+		}
+		return
+	}
+
+	if fn.Name == `\compact` || fn.Name == `\func_get_args` {
+		// there is no need to check the number of arguments for these functions.
+		return
+	}
+
+	if !enoughArgs(args, fn) {
+		b.report(n, LevelWarning, "argCount", "Too few arguments for %s", meta.NameNodeToString(n))
+	}
+}
+
+func (b *blockLinter) checkFunctionCall(e *ir.FunctionCallExpr) {
+	call := resolveFunctionCall(b.walker.ctx.sc, b.walker.r.ctx.st, b.walker.ctx.customTypes, e)
+	fqName := call.funcName
+
+	if call.canAnalyze {
+		b.checkCallArgs(e.Function, e.Args, call.info)
+		b.checkDeprecatedFunctionCall(e, &call)
+		b.checkFunctionAvailability(e, &call)
+		b.walker.r.checkNameCase(e.Function, call.funcName, call.info.Name)
+	}
+
 	switch fqName {
 	case `\preg_match`, `\preg_match_all`, `\preg_replace`, `\preg_split`:
 		if len(e.Args) < 1 {
@@ -669,6 +757,115 @@ func (b *blockLinter) checkFunctionCall(e *ir.FunctionCallExpr) {
 	}
 }
 
+func (b *blockLinter) checkMethodCall(e *ir.MethodCallExpr) {
+	parseState := b.walker.r.ctx.st
+
+	call := resolveMethodCall(b.walker.ctx.sc, parseState, b.walker.ctx.customTypes, e)
+	if !call.canAnalyze {
+		return
+	}
+
+	if !call.isMagic {
+		b.checkCallArgs(e.Method, e.Args, call.info)
+	}
+
+	if !call.isFound && !call.isMagic && !parseState.IsTrait && !b.walker.isThisInsideClosure(e.Variable) {
+		// The method is undefined but we permit calling it if `method_exists`
+		// was called prior to that call.
+		if !b.walker.ctx.customMethodExists(e.Variable, call.methodName) {
+			b.report(e.Method, LevelError, "undefined", "Call to undefined method {%s}->%s()", call.methodCallerType, call.methodName)
+		}
+	} else if !call.isMagic && !parseState.IsTrait {
+		// Method is defined.
+		b.walker.r.checkNameCase(e.Method, call.methodName, call.info.Name)
+		if call.info.IsStatic() {
+			b.report(e.Method, LevelWarning, "callStatic", "Calling static method as instance method")
+		}
+	}
+
+	if call.info.Doc.Deprecated {
+		if call.info.Doc.DeprecationNote != "" {
+			b.report(e.Method, LevelDoNotReject, "deprecated", "Call to deprecated method {%s}->%s() (%s)",
+				call.methodCallerType, call.methodName, call.info.Doc.DeprecationNote)
+		} else {
+			b.report(e.Method, LevelDoNotReject, "deprecated", "Call to deprecated method {%s}->%s()",
+				call.methodCallerType, call.methodName)
+		}
+	}
+
+	if call.isFound && !call.isMagic && !canAccess(parseState, call.className, call.info.AccessLevel) {
+		b.report(e.Method, LevelError, "accessLevel", "Cannot access %s method %s->%s()", call.info.AccessLevel, call.className, call.methodName)
+	}
+}
+
+func (b *blockLinter) checkStaticCall(e *ir.StaticCallExpr) {
+	call := resolveStaticMethodCall(b.walker.r.ctx.st, e)
+	if !call.canAnalyze {
+		return
+	}
+
+	if !call.isMagic {
+		b.checkCallArgs(e.Call, e.Args, call.methodInfo.Info)
+	}
+
+	if !call.isFound && !call.isMagic && !b.walker.r.ctx.st.IsTrait {
+		b.report(e.Call, LevelError, "undefined", "Call to undefined method %s::%s()", call.className, call.methodName)
+	} else if !call.isParentCall && !call.methodInfo.Info.IsStatic() && !call.isMagic && !b.walker.r.ctx.st.IsTrait {
+		// Method is defined.
+		// parent::f() is permitted.
+		b.report(e.Call, LevelWarning, "callStatic", "Calling instance method as static method")
+	}
+
+	if call.isFound && !canAccess(b.walker.r.ctx.st, call.methodInfo.ClassName, call.methodInfo.Info.AccessLevel) {
+		b.report(e.Call, LevelError, "accessLevel", "Cannot access %s method %s::%s()", call.methodInfo.Info.AccessLevel, call.methodInfo.ClassName, call.methodName)
+	}
+}
+
+func (b *blockLinter) checkPropertyFetch(e *ir.PropertyFetchExpr) {
+	fetch := resolvePropertyFetch(b.walker.ctx.sc, b.walker.r.ctx.st, b.walker.ctx.customTypes, e)
+	if !fetch.canAnalyze {
+		return
+	}
+
+	if !fetch.isFound && !fetch.isMagic && !b.walker.r.ctx.st.IsTrait && !b.walker.isThisInsideClosure(e.Variable) {
+		b.report(e.Property, LevelError, "undefined", "Property {%s}->%s does not exist", fetch.propertyFetchType, fetch.propertyNode.Value)
+	}
+
+	if fetch.isFound && !fetch.isMagic && !canAccess(b.walker.r.ctx.st, fetch.className, fetch.info.AccessLevel) {
+		b.report(e.Property, LevelError, "accessLevel", "Cannot access %s property %s->%s", fetch.info.AccessLevel, fetch.className, fetch.propertyNode.Value)
+	}
+}
+
+func (b *blockLinter) checkStaticPropertyFetch(e *ir.StaticPropertyFetchExpr) {
+	fetch := resolveStaticPropertyFetch(b.walker.r.ctx.st, e)
+	if !fetch.canAnalyze {
+		return
+	}
+
+	if !fetch.isFound && !b.walker.r.ctx.st.IsTrait {
+		b.report(e.Property, LevelError, "undefined", "Property %s::$%s does not exist", fetch.className, fetch.propertyName)
+	}
+
+	if fetch.isFound && !canAccess(b.walker.r.ctx.st, fetch.info.ClassName, fetch.info.Info.AccessLevel) {
+		b.report(e.Property, LevelError, "accessLevel", "Cannot access %s property %s::$%s", fetch.info.Info.AccessLevel, fetch.info.ClassName, fetch.propertyName)
+	}
+}
+
+func (b *blockLinter) checkClassConstFetch(e *ir.ClassConstFetchExpr) {
+	fetch := resolveClassConstFetch(b.walker.r.ctx.st, e)
+	if !fetch.canAnalyze {
+		return
+	}
+
+	if !fetch.isFound && !b.walker.r.ctx.st.IsTrait {
+		b.walker.r.Report(e.ConstantName, LevelError, "undefined", "Class constant %s::%s does not exist", fetch.className, fetch.constName)
+	}
+
+	if fetch.isFound && !canAccess(b.walker.r.ctx.st, fetch.implClassName, fetch.info.AccessLevel) {
+		b.walker.r.Report(e.ConstantName, LevelError, "accessLevel", "Cannot access %s constant %s::%s", fetch.info.AccessLevel, fetch.implClassName, fetch.constName)
+	}
+}
+
 func (b *blockLinter) checkInterfaceStmt(iface *ir.InterfaceStmt) {
 	for _, st := range iface.Stmts {
 		switch x := st.(type) {
diff --git a/src/linter/utils.go b/src/linter/utils.go
index 86b8e7c..867505c 100644
--- a/src/linter/utils.go
+++ b/src/linter/utils.go
@@ -2,6 +2,7 @@ package linter
 
 import (
 	"fmt"
+	"math"
 	"strings"
 	"unicode"
 	"unicode/utf8"
@@ -90,35 +91,35 @@ func typesMapToTypeExpr(p *phpdoc.TypeParser, m meta.TypesMap) phpdoc.Type {
 }
 
 type funcCallInfo struct {
-	canAnalyze bool
-	defined    bool
-	fqName     string
+	funcName   string
 	info       meta.FuncInfo
+	isFound    bool
+	canAnalyze bool
 }
 
 // TODO: bundle type solving params somehow.
 // We usually need ClassParseState+Scope+[]CustomType.
 func resolveFunctionCall(sc *meta.Scope, st *meta.ClassParseState, customTypes []solver.CustomType, call *ir.FunctionCallExpr) funcCallInfo {
 	var res funcCallInfo
-	res.canAnalyze = true
 	if !meta.IsIndexingComplete() {
 		return res
 	}
+	res.canAnalyze = true
 
 	fqName, ok := solver.GetFuncName(st, call.Function)
 	if ok {
-		res.fqName = fqName
-		res.info, res.defined = meta.Info.GetFunction(fqName)
+		res.funcName = fqName
+		res.info, res.isFound = meta.Info.GetFunction(fqName)
 	} else {
 		solver.ExprTypeCustom(sc, st, call.Function, customTypes).Iterate(func(typ string) {
-			if res.defined {
+			if res.isFound {
 				return
 			}
 			m, ok := solver.FindMethod(typ, `__invoke`)
 			res.info = m.Info
-			res.defined = ok
+			res.isFound = ok
 		})
-		if !res.defined {
+		if !res.isFound {
 			res.canAnalyze = false
 		}
 	}
@@ -126,6 +127,238 @@ func resolveFunctionCall(sc *meta.Scope, st *meta.ClassParseState, customTypes [
 	return res
 }
 
+type methodCallInfo struct {
+	methodName       string
+	className        string
+	info             meta.FuncInfo
+	methodCallerType meta.TypesMap
+	isFound          bool
+	isMagic          bool
+	canAnalyze       bool
+}
+
+func resolveMethodCall(sc *meta.Scope, st *meta.ClassParseState, customTypes []solver.CustomType, e *ir.MethodCallExpr) methodCallInfo {
+	if !meta.IsIndexingComplete() {
+		return methodCallInfo{canAnalyze: false}
+	}
+
+	var methodName string
+
+	switch id := e.Method.(type) {
+	case *ir.Identifier:
+		methodName = id.Value
+	default:
+		return methodCallInfo{canAnalyze: false}
+	}
+
+	var (
+		matchDist   = math.MaxInt32
+		foundMethod bool
+		magic       bool
+		fn          meta.FuncInfo
+		className   string
+	)
+
+	methodCallerType := solver.ExprTypeCustom(sc, st, e.Variable, customTypes)
+
+	methodCallerType.Find(func(typ string) bool {
+		m, isMagic, ok := findMethod(typ, methodName)
+		if !ok {
+			return false
+		}
+		foundMethod = true
+		if dist := classDistance(st, typ); dist < matchDist {
+			matchDist = dist
+			fn = m.Info
+			className = m.ClassName
+			magic = isMagic
+		}
+		return matchDist == 0 // Stop if found inside the current class
+	})
+
+	return methodCallInfo{
+		methodName:       methodName,
+		className:        className,
+		isFound:          foundMethod,
+		isMagic:          magic,
+		info:             fn,
+		methodCallerType: methodCallerType,
+		canAnalyze:       true,
+	}
+}
+
+type staticMethodCallInfo struct {
+	methodName               string
+	className                string
+	methodInfo               solver.FindMethodResult
+	isParentCall             bool
+	isMagic                  bool
+	isFound                  bool
+	isCallsParentConstructor bool
+	canAnalyze               bool
+}
+
+func resolveStaticMethodCall(st *meta.ClassParseState, e *ir.StaticCallExpr) staticMethodCallInfo {
+	if !meta.IsIndexingComplete() {
+		return staticMethodCallInfo{canAnalyze: false}
+	}
+
+	var methodName string
+
+	switch id := e.Call.(type) {
+	case *ir.Identifier:
+		methodName = id.Value
+	default:
+		return staticMethodCallInfo{canAnalyze: false}
+	}
+
+	classNameNode, ok := e.Class.(*ir.Name)
+	parentCall := ok && classNameNode.Value == "parent"
+	var callsParentConstructor bool
+	if parentCall && methodName == "__construct" {
+		callsParentConstructor = true
+	}
+
+	className, ok := solver.GetClassName(st, e.Class)
+	if !ok {
+		return staticMethodCallInfo{canAnalyze: false}
+	}
+
+	m, found := solver.FindMethod(className, methodName)
+	isMagic := haveMagicMethod(className, `__callStatic`)
+
+	return staticMethodCallInfo{
+		methodName:               methodName,
+		className:                className,
+		methodInfo:               m,
+		isMagic:                  isMagic,
+		isParentCall:             parentCall,
+		isFound:                  found,
+		isCallsParentConstructor: callsParentConstructor,
+		canAnalyze:               true,
+	}
+}
+
+type propertyFetchInfo struct {
+	className         string
+	info              meta.PropertyInfo
+	propertyFetchType meta.TypesMap
+	propertyNode      *ir.Identifier
+	isFound           bool
+	isMagic           bool
+	canAnalyze        bool
+}
+
+func resolvePropertyFetch(sc *meta.Scope, st *meta.ClassParseState, customTypes []solver.CustomType, e *ir.PropertyFetchExpr) propertyFetchInfo {
+	propertyNode, ok := e.Property.(*ir.Identifier)
+	if !ok {
+		return propertyFetchInfo{canAnalyze: false}
+	}
+
+	var found bool
+	var magic bool
+	var matchDist = math.MaxInt32
+	var className string
+	var info meta.PropertyInfo
+
+	propertyFetchType := solver.ExprTypeCustom(sc, st, e.Variable, customTypes)
+	propertyFetchType.Find(func(typ string) bool {
+		p, isMagic, ok := findProperty(typ, propertyNode.Value)
+		if !ok {
+			return false
+		}
+		found = true
+		if dist := classDistance(st, typ); dist < matchDist {
+			matchDist = dist
+			info = p.Info
+			className = p.ClassName
+			magic = isMagic
+		}
+		return matchDist == 0 // Stop if found inside the current class
+	})
+
+	return propertyFetchInfo{
+		className:         className,
+		isFound:           found,
+		isMagic:           magic,
+		info:              info,
+		propertyFetchType: propertyFetchType,
+		propertyNode:      propertyNode,
+		canAnalyze:        true,
+	}
+}
+
+type propertyStaticFetchInfo struct {
+	className       string
+	propertyName    string
+	info            solver.FindPropertyResult
+	isFound         bool
+	needHandleAsVar bool
+	canAnalyze      bool
+}
+
+func resolveStaticPropertyFetch(st *meta.ClassParseState, e *ir.StaticPropertyFetchExpr) propertyStaticFetchInfo {
+	if !meta.IsIndexingComplete() {
+		return propertyStaticFetchInfo{canAnalyze: false}
+	}
+
+	propertyNode, ok := e.Property.(*ir.SimpleVar)
+	if !ok {
+		return propertyStaticFetchInfo{needHandleAsVar: true, canAnalyze: false}
+	}
+
+	className, ok := solver.GetClassName(st, e.Class)
+	if !ok {
+		return propertyStaticFetchInfo{canAnalyze: false}
+	}
+
+	property, found := solver.FindProperty(className, "$"+propertyNode.Name)
+
+	return propertyStaticFetchInfo{
+		className:    className,
+		propertyName: propertyNode.Name,
+		info:         property,
+		isFound:      found,
+		canAnalyze:   true,
+	}
+}
+
+type classPropertyFetchInfo struct {
+	constName     string
+	className     string
+	implClassName string
+	info          meta.ConstInfo
+	isFound       bool
+	canAnalyze    bool
+}
+
+func resolveClassConstFetch(st *meta.ClassParseState, e *ir.ClassConstFetchExpr) classPropertyFetchInfo {
+	if !meta.IsIndexingComplete() {
+		return classPropertyFetchInfo{canAnalyze: false}
+	}
+
+	constName := e.ConstantName
+	if constName.Value == `class` || constName.Value == `CLASS` {
+		return classPropertyFetchInfo{canAnalyze: false}
+	}
+
+	className, ok := solver.GetClassName(st, e.Class)
+	if !ok {
+		return classPropertyFetchInfo{canAnalyze: false}
+	}
+
+	info, implClass, found := solver.FindConstant(className, constName.Value)
+
+	return classPropertyFetchInfo{
+		constName:     constName.Value,
+		className:     className,
+		implClassName: implClass,
+		info:          info,
+		isFound:       found,
+		canAnalyze:    true,
+	}
+}
+
 // isCapitalized reports whether s starts with an upper case letter.
 func isCapitalized(s string) bool {
 	ch, _ := utf8.DecodeRuneInString(s)


Test patch:
diff --git a/src/tests/checkers/oop_test.go b/src/tests/checkers/oop_test.go
index 751ef40..642f490 100644
--- a/src/tests/checkers/oop_test.go
+++ b/src/tests/checkers/oop_test.go
@@ -695,7 +695,7 @@ func TestClosureLateBinding(t *testing.T) {
 	`)
 	test.Expect = []string{
 		"Undefined variable: a",
-		"Call to undefined method {mixed}->method()",
+		"Call to undefined method {undefined}->method()",
 	}
 	linttest.RunFilterMatch(test, "undefined")
 }