Repository: VKCOM/noverify
Pull request #891 (2021-01-15 20:27:08)

linter: unexported RootWalker
Now Parse functions return ParseResult struct instead of RootWalker.


Patch:
diff --git a/src/linter/block.go b/src/linter/block.go
index ff1e97c..1aa616d 100644
--- a/src/linter/block.go
+++ b/src/linter/block.go
@@ -65,7 +65,7 @@ type blockWalker struct {
 	// inferred return types if any
 	returnTypes meta.TypesMap
 
-	r *RootWalker
+	r *rootWalker
 
 	custom []BlockChecker
 
@@ -104,7 +104,7 @@ type blockWalker struct {
 	parentBlockWalkers []*blockWalker // all parent block walkers if we handle nested arrow functions.
 }
 
-func newBlockWalker(r *RootWalker, sc *meta.Scope) *blockWalker {
+func newBlockWalker(r *rootWalker, sc *meta.Scope) *blockWalker {
 	b := &blockWalker{
 		r:            r,
 		ctx:          &blockContext{sc: sc},
diff --git a/src/linter/cache.go b/src/linter/cache.go
index 6e52a76..8fc2cb5 100644
--- a/src/linter/cache.go
+++ b/src/linter/cache.go
@@ -60,7 +60,7 @@ type fileMeta struct {
 	FunctionOverrides meta.FunctionsOverrideMap
 }
 
-func writeMetaCache(w *bufio.Writer, root *RootWalker) error {
+func writeMetaCache(w *bufio.Writer, root *rootWalker) error {
 	if err := writeMetaCacheHeader(w, root); err != nil {
 		return err
 	}
@@ -74,7 +74,7 @@ func writeMetaCache(w *bufio.Writer, root *RootWalker) error {
 	return nil
 }
 
-func createMetaCacheFile(filename, cacheFile string, root *RootWalker) error {
+func createMetaCacheFile(filename, cacheFile string, root *rootWalker) error {
 	tmpPath := cacheFile + ".tmp"
 	if err := os.MkdirAll(filepath.Dir(tmpPath), 0777); err != nil {
 		return err
@@ -160,7 +160,7 @@ func updateMetaInfo(filename string, m *fileMeta) {
 	}
 }
 
-func writeMetaCacheHeader(wr *bufio.Writer, root *RootWalker) error {
+func writeMetaCacheHeader(wr *bufio.Writer, root *rootWalker) error {
 	if err := wr.WriteByte(cacheVersion); err != nil {
 		return err
 	}
@@ -185,7 +185,7 @@ func writeMetaCacheHeader(wr *bufio.Writer, root *RootWalker) error {
 	return nil
 }
 
-func customCachersEncode(wr *bufio.Writer, root *RootWalker) error {
+func customCachersEncode(wr *bufio.Writer, root *rootWalker) error {
 	for i, c := range root.custom {
 		cacher := metaCachers[i]
 		if cacher == nil {
diff --git a/src/linter/custom.go b/src/linter/custom.go
index 59ca5f8..1d24721 100644
--- a/src/linter/custom.go
+++ b/src/linter/custom.go
@@ -124,7 +124,7 @@ func (RootCheckerDefaults) AfterLeaveNode(ir.Node)  {}
 
 // RootContext is the context for root checker to run on.
 type RootContext struct {
-	w *RootWalker
+	w *rootWalker
 }
 
 // ParsePHPDoc returns parsed phpdoc comment parts.
diff --git a/src/linter/root.go b/src/linter/root.go
index 768d999..732fef7 100644
--- a/src/linter/root.go
+++ b/src/linter/root.go
@@ -29,8 +29,8 @@ const (
 	maxFunctionLines = 150
 )
 
-// RootWalker is used to analyze root scope. Mostly defines, function and class definitions are analyzed.
-type RootWalker struct {
+// rootWalker is used to analyze root scope. Mostly defines, function and class definitions are analyzed.
+type rootWalker struct {
 	// An associated file that is traversed by the current Root Walker.
 	file *workspace.File
 
@@ -73,7 +73,7 @@ type phpDocParamEl struct {
 type phpDocParamsMap map[string]phpDocParamEl
 
 // InitCustom is needed to initialize walker state
-func (d *RootWalker) InitCustom() {
+func (d *rootWalker) InitCustom() {
 	d.custom = nil
 	for _, createFn := range customRootLinters {
 		d.custom = append(d.custom, createFn(&RootContext{w: d}))
@@ -82,13 +82,8 @@ func (d *RootWalker) InitCustom() {
 	d.customBlock = customBlockLinters
 }
 
-// UpdateMetaInfo is intended to be used in tests. Do not use it directly!
-func (d *RootWalker) UpdateMetaInfo() {
-	updateMetaInfo(d.ctx.st.CurrentFile, &d.meta)
-}
-
 // scope returns root-level variable scope if applicable.
-func (d *RootWalker) scope() *meta.Scope {
+func (d *rootWalker) scope() *meta.Scope {
 	if d.meta.Scope == nil {
 		d.meta.Scope = meta.NewScope()
 	}
@@ -96,25 +91,20 @@ func (d *RootWalker) scope() *meta.Scope {
 }
 
 // state allows for custom hooks to store state between entering root context and block context.
-func (d *RootWalker) state() map[string]interface{} {
+func (d *rootWalker) state() map[string]interface{} {
 	if d.customState == nil {
 		d.customState = make(map[string]interface{})
 	}
 	return d.customState
 }
 
-// GetReports returns collected reports for this file.
-func (d *RootWalker) GetReports() []*Report {
-	return d.reports
-}
-
 // File returns file for current root walker.
-func (d *RootWalker) File() *workspace.File {
+func (d *rootWalker) File() *workspace.File {
 	return d.file
 }
 
 // EnterNode is invoked at every node in hierarchy
-func (d *RootWalker) EnterNode(n ir.Node) (res bool) {
+func (d *rootWalker) EnterNode(n ir.Node) (res bool) {
 	res = true
 
 	for _, c := range d.custom {
@@ -276,7 +266,7 @@ func (d *RootWalker) EnterNode(n ir.Node) (res bool) {
 	return res
 }
 
-func (d *RootWalker) parseStartPos(pos *position.Position) (startLn []byte, startChar int) {
+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)
@@ -288,7 +278,7 @@ func (d *RootWalker) parseStartPos(pos *position.Position) (startLn []byte, star
 	return startLn, startChar
 }
 
-func (d *RootWalker) report(n ir.Node, lineNumber int, level int, checkName, msg string, args ...interface{}) {
+func (d *rootWalker) report(n ir.Node, lineNumber int, level int, checkName, msg string, args ...interface{}) {
 	if !meta.IsIndexingComplete() {
 		return
 	}
@@ -400,17 +390,17 @@ func (d *RootWalker) report(n ir.Node, lineNumber int, level int, checkName, msg
 }
 
 // Report registers a single report message about some found problem.
-func (d *RootWalker) Report(n ir.Node, level int, checkName, msg string, args ...interface{}) {
+func (d *rootWalker) Report(n ir.Node, level int, checkName, msg string, args ...interface{}) {
 	d.report(n, 0, level, checkName, msg, args...)
 }
 
 // ReportByLine registers a single report message about some found problem in lineNumber code line.
-func (d *RootWalker) ReportByLine(lineNumber int, level int, checkName, msg string, args ...interface{}) {
+func (d *rootWalker) ReportByLine(lineNumber int, level int, checkName, msg string, args ...interface{}) {
 	d.report(nil, lineNumber, level, checkName, msg, args...)
 }
 
 // reportHash computes the report signature hash for the baseline.
-func (d *RootWalker) reportHash(pos *position.Position, startLine []byte, checkName, msg string) uint64 {
+func (d *rootWalker) reportHash(pos *position.Position, startLine []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.
@@ -473,7 +463,7 @@ func (d *RootWalker) reportHash(pos *position.Position, startLine []byte, checkN
 	})
 }
 
-func (d *RootWalker) reportUndefinedVariable(v ir.Node, maybeHave bool) {
+func (d *rootWalker) reportUndefinedVariable(v ir.Node, maybeHave bool) {
 	sv, ok := v.(*ir.SimpleVar)
 	if !ok {
 		d.Report(v, LevelInformation, "undefined", "Unknown variable variable %s used",
@@ -492,7 +482,7 @@ func (d *RootWalker) reportUndefinedVariable(v ir.Node, maybeHave bool) {
 	}
 }
 
-func (d *RootWalker) handleComment(c freefloating.String) {
+func (d *rootWalker) handleComment(c freefloating.String) {
 	if c.StringType != freefloating.CommentType {
 		return
 	}
@@ -535,7 +525,7 @@ type handleFuncResult struct {
 	callsParentConstructor bool
 }
 
-func (d *RootWalker) handleArrowFuncExpr(params []meta.FuncParam, expr ir.Node, sc *meta.Scope, parentBlockWalker *blockWalker) handleFuncResult {
+func (d *rootWalker) handleArrowFuncExpr(params []meta.FuncParam, expr ir.Node, sc *meta.Scope, parentBlockWalker *blockWalker) handleFuncResult {
 	b := newBlockWalker(d, sc)
 	b.inArrowFunction = true
 	parentBlockWalker.parentBlockWalkers = append(parentBlockWalker.parentBlockWalkers, parentBlockWalker)
@@ -557,7 +547,7 @@ func (d *RootWalker) handleArrowFuncExpr(params []meta.FuncParam, expr ir.Node,
 	}
 }
 
-func (d *RootWalker) handleFuncStmts(params []meta.FuncParam, uses, stmts []ir.Node, sc *meta.Scope) handleFuncResult {
+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}))
@@ -631,7 +621,7 @@ func (d *RootWalker) handleFuncStmts(params []meta.FuncParam, uses, stmts []ir.N
 	}
 }
 
-func (d *RootWalker) checkParentConstructorCall(n ir.Node, parentConstructorCalled bool) {
+func (d *rootWalker) checkParentConstructorCall(n ir.Node, parentConstructorCalled bool) {
 	if !meta.IsIndexingComplete() {
 		return
 	}
@@ -650,7 +640,7 @@ func (d *RootWalker) checkParentConstructorCall(n ir.Node, parentConstructorCall
 	}
 }
 
-func (d *RootWalker) getElementPos(n ir.Node) meta.ElementPosition {
+func (d *rootWalker) getElementPos(n ir.Node) meta.ElementPosition {
 	pos := ir.GetPosition(n)
 	_, startChar := d.parseStartPos(pos)
 
@@ -670,7 +660,7 @@ type methodModifiers struct {
 	final       bool
 }
 
-func (d *RootWalker) parseMethodModifiers(meth *ir.ClassMethodStmt) (res methodModifiers) {
+func (d *rootWalker) parseMethodModifiers(meth *ir.ClassMethodStmt) (res methodModifiers) {
 	res.accessLevel = meta.Public
 
 	for _, m := range meth.Modifiers {
@@ -695,7 +685,7 @@ func (d *RootWalker) parseMethodModifiers(meth *ir.ClassMethodStmt) (res methodM
 	return res
 }
 
-func (d *RootWalker) checkMagicMethod(meth ir.Node, name string, modif methodModifiers, countArgs int) {
+func (d *rootWalker) checkMagicMethod(meth ir.Node, name string, modif methodModifiers, countArgs int) {
 	const Any = -1
 	var (
 		canBeStatic    bool
@@ -774,7 +764,7 @@ func (d *RootWalker) checkMagicMethod(meth ir.Node, name string, modif methodMod
 	}
 }
 
-func (d *RootWalker) getClass() meta.ClassInfo {
+func (d *rootWalker) getClass() meta.ClassInfo {
 	var m meta.ClassesMap
 
 	if d.ctx.st.IsTrait {
@@ -809,7 +799,7 @@ func (d *RootWalker) getClass() meta.ClassInfo {
 	return cl
 }
 
-func (d *RootWalker) lowerCaseModifier(m *ir.Identifier) string {
+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",
@@ -818,7 +808,7 @@ func (d *RootWalker) lowerCaseModifier(m *ir.Identifier) string {
 	return lcase
 }
 
-func (d *RootWalker) enterPropertyList(pl *ir.PropertyListStmt) bool {
+func (d *rootWalker) enterPropertyList(pl *ir.PropertyListStmt) bool {
 	cl := d.getClass()
 
 	isStatic := false
@@ -869,7 +859,7 @@ func (d *RootWalker) enterPropertyList(pl *ir.PropertyListStmt) bool {
 	return true
 }
 
-func (d *RootWalker) enterClassConstList(s *ir.ClassConstListStmt) bool {
+func (d *rootWalker) enterClassConstList(s *ir.ClassConstListStmt) bool {
 	cl := d.getClass()
 	accessLevel := meta.Public
 
@@ -905,7 +895,7 @@ func (d *RootWalker) enterClassConstList(s *ir.ClassConstListStmt) bool {
 	return true
 }
 
-func (d *RootWalker) checkOldStyleConstructor(meth *ir.ClassMethodStmt, nm string) {
+func (d *rootWalker) checkOldStyleConstructor(meth *ir.ClassMethodStmt, nm string) {
 	lastDelim := strings.IndexByte(d.ctx.st.CurrentClass, '\\')
 	if strings.EqualFold(d.ctx.st.CurrentClass[lastDelim+1:], nm) {
 		_, isClass := d.currentClassNode.(*ir.ClassStmt)
@@ -915,7 +905,7 @@ func (d *RootWalker) checkOldStyleConstructor(meth *ir.ClassMethodStmt, nm strin
 	}
 }
 
-func (d *RootWalker) enterClassMethod(meth *ir.ClassMethodStmt) bool {
+func (d *rootWalker) enterClassMethod(meth *ir.ClassMethodStmt) bool {
 	nm := meth.MethodName.Value
 	_, insideInterface := d.currentClassNode.(*ir.InterfaceStmt)
 
@@ -1047,7 +1037,7 @@ func (d *RootWalker) enterClassMethod(meth *ir.ClassMethodStmt) bool {
 	return false
 }
 
-func (d *RootWalker) reportPhpdocErrors(n ir.Node, errs phpdocErrors) {
+func (d *rootWalker) reportPhpdocErrors(n ir.Node, errs phpdocErrors) {
 	for _, err := range errs.phpdocLint {
 		d.Report(n, LevelInformation, "phpdocLint", "%s", err)
 	}
@@ -1056,7 +1046,7 @@ func (d *RootWalker) reportPhpdocErrors(n ir.Node, errs phpdocErrors) {
 	}
 }
 
-func (d *RootWalker) parsePHPDocVar(n ir.Node, doc []phpdoc.CommentPart) (m meta.TypesMap) {
+func (d *rootWalker) parsePHPDocVar(n ir.Node, doc []phpdoc.CommentPart) (m meta.TypesMap) {
 	for _, part := range doc {
 		d.checkPHPDocRef(n, part)
 		part, ok := part.(*phpdoc.TypeVarCommentPart)
@@ -1079,7 +1069,7 @@ type phpDocParseResult struct {
 	errs       phpdocErrors
 }
 
-func (d *RootWalker) isValidPHPDocRef(n ir.Node, ref string) bool {
+func (d *rootWalker) isValidPHPDocRef(n ir.Node, ref string) bool {
 	// Skip:
 	// - URLs
 	// - Things that can be a filename (e.g. "foo.php")
@@ -1173,7 +1163,7 @@ func (d *RootWalker) isValidPHPDocRef(n ir.Node, ref string) bool {
 	}
 }
 
-func (d *RootWalker) checkPHPDocRef(n ir.Node, part phpdoc.CommentPart) {
+func (d *rootWalker) checkPHPDocRef(n ir.Node, part phpdoc.CommentPart) {
 	if !meta.IsIndexingComplete() {
 		return
 	}
@@ -1186,7 +1176,7 @@ func (d *RootWalker) checkPHPDocRef(n ir.Node, part phpdoc.CommentPart) {
 	}
 }
 
-func (d *RootWalker) checkPHPDocSeeRef(n ir.Node, part phpdoc.CommentPart) {
+func (d *rootWalker) checkPHPDocSeeRef(n ir.Node, part phpdoc.CommentPart) {
 	params := part.(*phpdoc.RawCommentPart).Params
 	if len(params) == 0 {
 		return
@@ -1211,7 +1201,7 @@ func (d *RootWalker) checkPHPDocSeeRef(n ir.Node, part phpdoc.CommentPart) {
 	}
 }
 
-func (d *RootWalker) checkPHPDocMixinRef(n ir.Node, part phpdoc.CommentPart) {
+func (d *rootWalker) checkPHPDocMixinRef(n ir.Node, part phpdoc.CommentPart) {
 	rawPart, ok := part.(*phpdoc.RawCommentPart)
 	if !ok {
 		return
@@ -1235,7 +1225,7 @@ func (d *RootWalker) checkPHPDocMixinRef(n ir.Node, part phpdoc.CommentPart) {
 	}
 }
 
-func (d *RootWalker) parsePHPDoc(n ir.Node, doc []phpdoc.CommentPart, actualParams []ir.Node) phpDocParseResult {
+func (d *rootWalker) parsePHPDoc(n ir.Node, doc []phpdoc.CommentPart, actualParams []ir.Node) phpDocParseResult {
 	var result phpDocParseResult
 
 	if len(doc) == 0 {
@@ -1329,7 +1319,7 @@ func (d *RootWalker) parsePHPDoc(n ir.Node, doc []phpdoc.CommentPart, actualPara
 }
 
 // parse type info, e.g. "string" in "someFunc() : string { ... }"
-func (d *RootWalker) parseTypeNode(n ir.Node) (typ meta.TypesMap, ok bool) {
+func (d *rootWalker) parseTypeNode(n ir.Node) (typ meta.TypesMap, ok bool) {
 	if n == nil {
 		return meta.TypesMap{}, false
 	}
@@ -1340,7 +1330,7 @@ func (d *RootWalker) parseTypeNode(n ir.Node) (typ meta.TypesMap, ok bool) {
 }
 
 // callbackParamByIndex returns the description of the parameter for the function by its index.
-func (d *RootWalker) callbackParamByIndex(param ir.Node, argType meta.TypesMap) meta.FuncParam {
+func (d *rootWalker) callbackParamByIndex(param ir.Node, argType meta.TypesMap) meta.FuncParam {
 	p := param.(*ir.Parameter)
 	v := p.Variable
 
@@ -1360,7 +1350,7 @@ func (d *RootWalker) callbackParamByIndex(param ir.Node, argType meta.TypesMap)
 	return arg
 }
 
-func (d *RootWalker) parseFuncArgsForCallback(params []ir.Node, sc *meta.Scope, closureSolver *solver.ClosureCallerInfo) (args []meta.FuncParam, minArgs int) {
+func (d *rootWalker) parseFuncArgsForCallback(params []ir.Node, sc *meta.Scope, closureSolver *solver.ClosureCallerInfo) (args []meta.FuncParam, minArgs int) {
 	countParams := len(params)
 	minArgs = countParams
 	if countParams == 0 {
@@ -1396,7 +1386,7 @@ func (d *RootWalker) parseFuncArgsForCallback(params []ir.Node, sc *meta.Scope,
 	return args, minArgs
 }
 
-func (d *RootWalker) parseFuncArgs(params []ir.Node, parTypes phpDocParamsMap, sc *meta.Scope, closureSolver *solver.ClosureCallerInfo) (args []meta.FuncParam, minArgs int) {
+func (d *rootWalker) parseFuncArgs(params []ir.Node, parTypes phpDocParamsMap, sc *meta.Scope, closureSolver *solver.ClosureCallerInfo) (args []meta.FuncParam, minArgs int) {
 	if len(params) == 0 {
 		return nil, 0
 	}
@@ -1453,18 +1443,18 @@ func (d *RootWalker) parseFuncArgs(params []ir.Node, parTypes phpDocParamsMap, s
 	return args, minArgs
 }
 
-func (d *RootWalker) checkCommentMisspellings(n ir.Node, s string) {
+func (d *rootWalker) checkCommentMisspellings(n ir.Node, s string) {
 	// Try to avoid checking for symbol names and references.
 	d.checkMisspellings(n, s, "misspellComment", isCapitalized)
 }
 
-func (d *RootWalker) checkVarnameMisspellings(n ir.Node, s string) {
+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) {
+func (d *rootWalker) checkIdentMisspellings(n *ir.Identifier) {
 	d.checkMisspellings(n, n.Value, "misspellName", func(s string) bool {
 		// Before PHP got context-sensitive lexer, it was common to use
 		// method names to avoid parsing errors.
@@ -1474,7 +1464,7 @@ func (d *RootWalker) checkIdentMisspellings(n *ir.Identifier) {
 	})
 }
 
-func (d *RootWalker) checkMisspellings(n ir.Node, s string, label string, skip func(string) bool) {
+func (d *rootWalker) checkMisspellings(n ir.Node, s string, label string, skip func(string) bool) {
 	if !meta.IsIndexingComplete() {
 		return
 	}
@@ -1490,7 +1480,7 @@ func (d *RootWalker) checkMisspellings(n ir.Node, s string, label string, skip f
 	}
 }
 
-func (d *RootWalker) enterFunction(fun *ir.FunctionStmt) bool {
+func (d *rootWalker) enterFunction(fun *ir.FunctionStmt) bool {
 	nm := d.ctx.st.Namespace + `\` + fun.FunctionName.Value
 	pos := ir.GetPosition(fun)
 
@@ -1549,7 +1539,7 @@ func (d *RootWalker) enterFunction(fun *ir.FunctionStmt) bool {
 	return false
 }
 
-func (d *RootWalker) checkFuncParam(p *ir.Parameter) {
+func (d *rootWalker) checkFuncParam(p *ir.Parameter) {
 	// TODO(quasilyte): DefaultValue can only contain constant expressions.
 	// Could run special check over them to detect the potential fatal errors.
 	walkNode(p.DefaultValue, func(w ir.Node) bool {
@@ -1560,7 +1550,7 @@ func (d *RootWalker) checkFuncParam(p *ir.Parameter) {
 	})
 }
 
-func (d *RootWalker) enterFunctionCall(s *ir.FunctionCallExpr) bool {
+func (d *rootWalker) enterFunctionCall(s *ir.FunctionCallExpr) bool {
 	nm, ok := s.Function.(*ir.Name)
 	if !ok {
 		return true
@@ -1600,7 +1590,7 @@ func (d *RootWalker) enterFunctionCall(s *ir.FunctionCallExpr) bool {
 
 // 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 {
+func (d *rootWalker) handleOverride(s *ir.FunctionCallExpr) bool {
 	if len(s.Args) != 2 {
 		return true
 	}
@@ -1668,7 +1658,7 @@ func (d *RootWalker) handleOverride(s *ir.FunctionCallExpr) bool {
 	return true
 }
 
-func (d *RootWalker) enterConstList(lst *ir.ConstListStmt) bool {
+func (d *rootWalker) enterConstList(lst *ir.ConstListStmt) bool {
 	if d.meta.Constants == nil {
 		d.meta.Constants = make(meta.ConstantsMap)
 	}
@@ -1692,7 +1682,7 @@ func (d *RootWalker) enterConstList(lst *ir.ConstListStmt) bool {
 }
 
 // LeaveNode is invoked after node process
-func (d *RootWalker) LeaveNode(n ir.Node) {
+func (d *rootWalker) LeaveNode(n ir.Node) {
 	for _, c := range d.custom {
 		c.BeforeLeaveNode(n)
 	}
@@ -1711,7 +1701,7 @@ func (d *RootWalker) LeaveNode(n ir.Node) {
 	}
 }
 
-func (d *RootWalker) runRules(n ir.Node, sc *meta.Scope, rlist []rules.Rule) {
+func (d *rootWalker) runRules(n ir.Node, sc *meta.Scope, rlist []rules.Rule) {
 	for i := range rlist {
 		rule := &rlist[i]
 		if d.runRule(n, sc, rule) {
@@ -1725,7 +1715,7 @@ func (d *RootWalker) runRules(n ir.Node, sc *meta.Scope, rlist []rules.Rule) {
 	}
 }
 
-func (d *RootWalker) sourceNodeString(n ir.Node) string {
+func (d *rootWalker) sourceNodeString(n ir.Node) string {
 	pos := ir.GetPosition(n)
 	from := pos.StartPos
 	to := pos.EndPos
@@ -1739,7 +1729,7 @@ func (d *RootWalker) sourceNodeString(n ir.Node) string {
 	return irutil.FmtNode(n)
 }
 
-func (d *RootWalker) renderRuleMessage(msg string, n ir.Node, m phpgrep.MatchData, truncate bool) string {
+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, "$$") {
 		msg = strings.ReplaceAll(msg, "$$", d.sourceNodeString(n))
@@ -1767,7 +1757,7 @@ func (d *RootWalker) renderRuleMessage(msg string, n ir.Node, m phpgrep.MatchDat
 	return msg
 }
 
-func (d *RootWalker) runRule(n ir.Node, sc *meta.Scope, rule *rules.Rule) bool {
+func (d *rootWalker) runRule(n ir.Node, sc *meta.Scope, rule *rules.Rule) bool {
 	m, ok := rule.Matcher.Match(n)
 	if !ok {
 		return false
@@ -1817,7 +1807,7 @@ func (d *RootWalker) runRule(n ir.Node, sc *meta.Scope, rule *rules.Rule) bool {
 	return true
 }
 
-func (d *RootWalker) checkTypeFilter(wantType *phpdoc.Type, sc *meta.Scope, nn ir.Node) bool {
+func (d *rootWalker) checkTypeFilter(wantType *phpdoc.Type, sc *meta.Scope, nn ir.Node) bool {
 	if wantType == nil {
 		return true
 	}
@@ -1830,7 +1820,7 @@ func (d *RootWalker) checkTypeFilter(wantType *phpdoc.Type, sc *meta.Scope, nn i
 	return rules.TypeIsCompatible(wantType.Expr, haveType.Expr)
 }
 
-func (d *RootWalker) checkFilterSet(m *phpgrep.MatchData, sc *meta.Scope, filterSet map[string]rules.Filter) bool {
+func (d *rootWalker) checkFilterSet(m *phpgrep.MatchData, sc *meta.Scope, filterSet map[string]rules.Filter) bool {
 	// TODO: pass custom types here, so both @type and @pure predicates can use it.
 
 	for name, filter := range filterSet {
@@ -1850,7 +1840,7 @@ func (d *RootWalker) checkFilterSet(m *phpgrep.MatchData, sc *meta.Scope, filter
 	return true
 }
 
-func (d *RootWalker) checkTraitImplemented(n ir.Node, nameUsed string) {
+func (d *rootWalker) checkTraitImplemented(n ir.Node, nameUsed string) {
 	if !meta.IsIndexingComplete() {
 		return
 	}
@@ -1862,7 +1852,7 @@ func (d *RootWalker) checkTraitImplemented(n ir.Node, nameUsed string) {
 	d.checkImplemented(n, nameUsed, trait)
 }
 
-func (d *RootWalker) checkClassImplemented(n ir.Node, nameUsed string) {
+func (d *rootWalker) checkClassImplemented(n ir.Node, nameUsed string) {
 	if !meta.IsIndexingComplete() {
 		return
 	}
@@ -1874,11 +1864,11 @@ func (d *RootWalker) checkClassImplemented(n ir.Node, nameUsed string) {
 	d.checkImplemented(n, nameUsed, class)
 }
 
-func (d *RootWalker) checkIfaceImplemented(n ir.Node, nameUsed string) {
+func (d *rootWalker) checkIfaceImplemented(n ir.Node, nameUsed string) {
 	d.checkClassImplemented(n, nameUsed)
 }
 
-func (d *RootWalker) checkImplemented(n ir.Node, nameUsed string, otherClass meta.ClassInfo) {
+func (d *rootWalker) checkImplemented(n ir.Node, nameUsed string, otherClass meta.ClassInfo) {
 	cl := d.getClass()
 	if d.ctx.st.IsTrait || cl.IsAbstract() {
 		return
@@ -1888,7 +1878,7 @@ func (d *RootWalker) checkImplemented(n ir.Node, nameUsed string, otherClass met
 	d.checkImplementedStep(n, nameUsed, otherClass, visited)
 }
 
-func (d *RootWalker) checkImplementedStep(n ir.Node, className string, otherClass meta.ClassInfo, visited map[string]struct{}) {
+func (d *rootWalker) checkImplementedStep(n ir.Node, className string, otherClass meta.ClassInfo, visited map[string]struct{}) {
 	// TODO: check that method signatures are compatible?
 	if _, ok := visited[className]; ok {
 		return
@@ -1920,11 +1910,11 @@ func (d *RootWalker) checkImplementedStep(n ir.Node, className string, otherClas
 	}
 }
 
-func (d *RootWalker) reportUndefinedType(n ir.Node, name string) {
+func (d *rootWalker) reportUndefinedType(n ir.Node, name string) {
 	d.Report(n, LevelError, "undefined", "Type %s not found", name)
 }
 
-func (d *RootWalker) checkNameCase(n ir.Node, nameUsed, nameExpected string) {
+func (d *rootWalker) checkNameCase(n ir.Node, nameUsed, nameExpected string) {
 	if nameUsed == "" || nameExpected == "" {
 		return
 	}
@@ -1934,7 +1924,7 @@ func (d *RootWalker) checkNameCase(n ir.Node, nameUsed, nameExpected string) {
 	}
 }
 
-func (d *RootWalker) checkKeywordCasePos(n ir.Node, begin int, keyword string) {
+func (d *rootWalker) checkKeywordCasePos(n ir.Node, begin int, keyword string) {
 	from := begin
 	to := from + len(keyword)
 
@@ -1946,18 +1936,18 @@ func (d *RootWalker) checkKeywordCasePos(n ir.Node, begin int, keyword string) {
 	}
 }
 
-func (d *RootWalker) checkKeywordCase(n ir.Node, keyword string) {
+func (d *rootWalker) checkKeywordCase(n ir.Node, keyword string) {
 	// Only works for nodes that have a keyword of interest
 	// as the leftmost token.
 	d.checkKeywordCasePos(n, ir.GetPosition(n).StartPos, keyword)
 }
 
-func (d *RootWalker) nodeText(n ir.Node) []byte {
+func (d *rootWalker) nodeText(n ir.Node) []byte {
 	pos := ir.GetPosition(n)
 	return d.file.Contents()[pos.StartPos:pos.EndPos]
 }
 
-func (d *RootWalker) parseClassPHPDoc(n ir.Node, doc []phpdoc.CommentPart) classPhpDocParseResult {
+func (d *rootWalker) parseClassPHPDoc(n ir.Node, doc []phpdoc.CommentPart) classPhpDocParseResult {
 	var result classPhpDocParseResult
 
 	if len(doc) == 0 {
@@ -1985,13 +1975,13 @@ func (d *RootWalker) parseClassPHPDoc(n ir.Node, doc []phpdoc.CommentPart) class
 	return result
 }
 
-func (d *RootWalker) beforeEnterFile() {
+func (d *rootWalker) beforeEnterFile() {
 	for _, c := range d.custom {
 		c.BeforeEnterFile()
 	}
 }
 
-func (d *RootWalker) afterLeaveFile() {
+func (d *rootWalker) afterLeaveFile() {
 	for _, c := range d.custom {
 		c.AfterLeaveFile()
 	}
diff --git a/src/linter/worker.go b/src/linter/worker.go
index 0103af3..d9e0a19 100644
--- a/src/linter/worker.go
+++ b/src/linter/worker.go
@@ -26,6 +26,13 @@ import (
 	"github.com/VKCOM/noverify/src/workspace"
 )
 
+type ParseResult struct {
+	RootNode *ir.Root
+	Reports  []*Report
+
+	walker *rootWalker
+}
+
 // Worker is a linter handle that is expected to be executed in a single goroutine context.
 //
 // It's not thread-safe and contains the state that will be re-used between the linter API calls.
@@ -77,7 +84,7 @@ func (w *Worker) ID() int { return w.id }
 
 // ParseContents parses specified contents (or file) and returns *RootWalker.
 // Function does not update global meta.
-func (w *Worker) ParseContents(fileInfo workspace.FileInfo) (root *ir.Root, walker *RootWalker, err error) {
+func (w *Worker) ParseContents(fileInfo workspace.FileInfo) (result ParseResult, err error) {
 	defer func() {
 		if r := recover(); r != nil {
 			s := fmt.Sprintf("Panic while parsing %s: %s\n\nStack trace: %s", fileInfo.Name, r, dbg.Stack())
@@ -105,7 +112,7 @@ func (w *Worker) ParseContents(fileInfo workspace.FileInfo) (root *ir.Root, walk
 	b := w.ctx.scratchBuf
 	b.Reset()
 	if _, err := b.ReadFrom(rd); err != nil {
-		return nil, nil, err
+		return result, err
 	}
 	contents := append(make([]byte, 0, b.Len()), b.Bytes()...)
 
@@ -119,15 +126,24 @@ func (w *Worker) ParseContents(fileInfo workspace.FileInfo) (root *ir.Root, walk
 	atomic.AddInt64(&initParseTime, int64(time.Since(start)))
 
 	file := workspace.NewFile(fileInfo.Name, contents)
-	return w.analyzeFile(file, parser)
+	rootNode, walker, err := w.analyzeFile(file, parser)
+	if err != nil {
+		return result, err
+	}
+	result = ParseResult{
+		RootNode: rootNode,
+		Reports:  walker.reports,
+		walker:   walker,
+	}
+	return result, nil
 }
 
 // IndexFile parses the file and fills in the meta info. Can use cache.
 func (w *Worker) IndexFile(file workspace.FileInfo) error {
 	if CacheDir == "" {
-		_, w, err := w.ParseContents(file)
+		result, err := w.ParseContents(file)
 		if w != nil {
-			updateMetaInfo(file.Name, &w.meta)
+			updateMetaInfo(file.Name, &result.walker.meta)
 		}
 		return err
 	}
@@ -167,12 +183,12 @@ func (w *Worker) IndexFile(file workspace.FileInfo) error {
 	start := time.Now()
 	fp, err := os.Open(cacheFile)
 	if err != nil {
-		_, w, err := w.ParseContents(file)
+		result, err := w.ParseContents(file)
 		if err != nil {
 			return err
 		}
 
-		return createMetaCacheFile(file.Name, cacheFile, w)
+		return createMetaCacheFile(file.Name, cacheFile, result.walker)
 	}
 	defer fp.Close()
 
@@ -180,12 +196,12 @@ func (w *Worker) IndexFile(file workspace.FileInfo) error {
 		// do not really care about why exactly reading from cache failed
 		os.Remove(cacheFile)
 
-		_, w, err := w.ParseContents(file)
+		result, err := w.ParseContents(file)
 		if err != nil {
 			return err
 		}
 
-		return createMetaCacheFile(file.Name, cacheFile, w)
+		return createMetaCacheFile(file.Name, cacheFile, result.walker)
 	}
 
 	atomic.AddInt64(&initCacheReadTime, int64(time.Since(start)))
@@ -207,10 +223,10 @@ func (w *Worker) doParseFile(f workspace.FileInfo) []*Report {
 	var reports []*Report
 
 	if w.needReports {
-		var walker *RootWalker
-		_, walker, err = w.ParseContents(f)
+		var result ParseResult
+		result, err = w.ParseContents(f)
 		if err == nil {
-			reports = walker.GetReports()
+			reports = result.Reports
 		}
 	} else {
 		err = w.IndexFile(f)
@@ -224,7 +240,7 @@ func (w *Worker) doParseFile(f workspace.FileInfo) []*Report {
 	return reports
 }
 
-func (w *Worker) analyzeFile(file *workspace.File, parser *php7.Parser) (*ir.Root, *RootWalker, error) {
+func (w *Worker) analyzeFile(file *workspace.File, parser *php7.Parser) (*ir.Root, *rootWalker, error) {
 	start := time.Now()
 	rootNode := parser.GetRootNode()
 
@@ -236,7 +252,7 @@ func (w *Worker) analyzeFile(file *workspace.File, parser *php7.Parser) (*ir.Roo
 	rootIR := w.irconv.ConvertRoot(rootNode)
 
 	st := &meta.ClassParseState{CurrentFile: file.Name()}
-	walker := &RootWalker{
+	walker := &rootWalker{
 		file: file,
 		ctx:  newRootContext(w.ctx, st),
 
@@ -284,7 +300,7 @@ func (w *Worker) analyzeFile(file *workspace.File, parser *php7.Parser) (*ir.Roo
 // analyzeFileRootLevel does analyze file top-level code.
 // This method is exposed for language server use, you usually
 // do not need to call it yourself.
-func analyzeFileRootLevel(rootNode ir.Node, d *RootWalker) {
+func analyzeFileRootLevel(rootNode ir.Node, d *rootWalker) {
 	sc := meta.NewScope()
 	sc.AddVarName("argv", meta.NewTypesMap("string[]"), "predefined", meta.VarAlwaysDefined)
 	sc.AddVarName("argc", meta.NewTypesMap("int"), "predefined", meta.VarAlwaysDefined)


Test patch:
diff --git a/src/linter/cache_test.go b/src/linter/cache_test.go
index 9de8ce8..c2e9493 100644
--- a/src/linter/cache_test.go
+++ b/src/linter/cache_test.go
@@ -14,7 +14,6 @@ import (
 	"github.com/google/go-cmp/cmp"
 
 	"github.com/VKCOM/noverify/src/git"
-	"github.com/VKCOM/noverify/src/ir"
 	"github.com/VKCOM/noverify/src/linttest/assert"
 	"github.com/VKCOM/noverify/src/workspace"
 )
@@ -120,13 +119,13 @@ main();
 `
 
 	runTest := func(iteration int) {
-		_, root, err := parseContents("cachetest.php", []byte(code), nil, nil)
+		result, err := parseContents("cachetest.php", []byte(code), nil, nil)
 		if err != nil {
 			t.Fatalf("parse error: %v", err)
 		}
 		var buf bytes.Buffer
 		wr := bufio.NewWriter(&buf)
-		if err := writeMetaCache(wr, root); err != nil {
+		if err := writeMetaCache(wr, result.walker); err != nil {
 			t.Fatalf("write cache: %v", err)
 		}
 		wr.Flush()
@@ -159,7 +158,7 @@ main();
 		// 3. Check meta decoding.
 		//
 		// If it fails, encoding and/or decoding is broken.
-		encodedMeta := &root.meta
+		encodedMeta := &result.walker.meta
 		decodedMeta := &fileMeta{}
 		if err := readMetaCache(bytes.NewReader(buf.Bytes()), "", decodedMeta); err != nil {
 			t.Errorf("decoding failed: %v", err)
@@ -192,7 +191,7 @@ func collectCacheStrings(data string) string {
 	return hex.EncodeToString(enc.Sum(nil))
 }
 
-func parseContents(filename string, contents []byte, lineRanges []git.LineRange, allowDisabled *regexp.Regexp) (root *ir.Root, walker *RootWalker, err error) {
+func parseContents(filename string, contents []byte, lineRanges []git.LineRange, allowDisabled *regexp.Regexp) (ParseResult, error) {
 	w := NewLintingWorker(0)
 	w.AllowDisable = allowDisabled
 	file := workspace.FileInfo{
diff --git a/src/linter/phpdoc_test.go b/src/linter/phpdoc_test.go
index 4075c58..1fca2ee 100644
--- a/src/linter/phpdoc_test.go
+++ b/src/linter/phpdoc_test.go
@@ -59,7 +59,7 @@ func TestParseClassPHPDoc(t *testing.T) {
 	}
 
 	st := &meta.ClassParseState{}
-	walker := RootWalker{ctx: newRootContext(NewWorkerContext(), st)}
+	walker := rootWalker{ctx: newRootContext(NewWorkerContext(), st)}
 	for _, test := range tests {
 		docString := fmt.Sprintf(`/** %s */`, test.line)
 		doc := phpdoc.Parse(walker.ctx.phpdocTypeParser, docString)
diff --git a/src/linttest/linttest.go b/src/linttest/linttest.go
index f3645ef..405fcbf 100644
--- a/src/linttest/linttest.go
+++ b/src/linttest/linttest.go
@@ -16,7 +16,6 @@ import (
 	"time"
 
 	"github.com/VKCOM/noverify/src/cmd"
-	"github.com/VKCOM/noverify/src/ir"
 	"github.com/VKCOM/noverify/src/linter"
 	"github.com/VKCOM/noverify/src/meta"
 	"github.com/VKCOM/noverify/src/rules"
@@ -251,8 +250,8 @@ func (s *Suite) RunLinter() []*linter.Report {
 			continue
 		}
 
-		_, w := parseTestFile(s.t, linting, f)
-		reports = append(reports, w.GetReports()...)
+		result := parseTestFile(s.t, linting, f)
+		reports = append(reports, result.Reports...)
 	}
 
 	declared := make(map[string]struct{})
@@ -291,7 +290,7 @@ func (s *Suite) RunFilterLinter(filters []string) []*linter.Report {
 }
 
 // ParseTestFile parses given test file.
-func ParseTestFile(t *testing.T, filename, content string) (rootNode *ir.Root, w *linter.RootWalker) {
+func ParseTestFile(t *testing.T, filename, content string) linter.ParseResult {
 	var worker *linter.Worker
 	if meta.IsIndexingComplete() {
 		worker = linter.NewLintingWorker(0)
@@ -366,20 +365,22 @@ func shuffleFiles(files []TestFile) {
 	})
 }
 
-func parseTestFile(t testing.TB, worker *linter.Worker, f TestFile) (rootNode *ir.Root, w *linter.RootWalker) {
-	var err error
+func parseTestFile(t testing.TB, worker *linter.Worker, f TestFile) linter.ParseResult {
 	file := workspace.FileInfo{
 		Name:     f.Name,
 		Contents: f.Data,
 	}
-	rootNode, w, err = worker.ParseContents(file)
+
+	var err error
+	var result linter.ParseResult
+	if meta.IsIndexingComplete() {
+		result, err = worker.ParseContents(file)
+	} else {
+		err = worker.IndexFile(file)
+	}
 	if err != nil {
 		t.Fatalf("could not parse %s: %v", f.Name, err.Error())
 	}
 
-	if !meta.IsIndexingComplete() {
-		w.UpdateMetaInfo()
-	}
-
-	return rootNode, w
+	return result
 }
diff --git a/src/tests/exprtype/exprtype_test.go b/src/tests/exprtype/exprtype_test.go
index 16a4d59..5bcdd9d 100644
--- a/src/tests/exprtype/exprtype_test.go
+++ b/src/tests/exprtype/exprtype_test.go
@@ -2947,13 +2947,13 @@ func runExprTypeTest(t *testing.T, params *exprTypeTestParams) {
 
 	// Reset results map and run expr type collector.
 	exprTypeResult = map[ir.Node]meta.TypesMap{}
-	root, _ := linttest.ParseTestFile(t, "exprtype.php", params.code)
+	result := linttest.ParseTestFile(t, "exprtype.php", params.code)
 
 	// Check that collected types are identical to the expected types.
 	// We need the second walker to pass *testing.T parameter to
 	// the walker that does the comparison.
 	walker := exprTypeWalker{t: t}
-	root.Walk(&walker)
+	result.RootNode.Walk(&walker)
 }
 
 type testTypesMap struct {