Repository: VKCOM/noverify
Pull request #1053 (2021-07-12 19:06:07)

linter: refactored report functions
Now at the heart of all the functions for creating reports
is a function that creates a report at a given location.

`Report` and `ReportPHPDoc` are now wrappers that convert `node`
and `PHPDocLocation` to `Location` for `ReportLocation`.

`ReportByLine` has been removed as it is no longer used and
can be replaced with `ReportLocation`.

Fixed error, if the warning was given on the last line in
the file, then the context line was empty.

A new `ir.Location` structure has been added.
`ir.Location` stores the shifts relative to the current line.
A new structure is needed since `position.Position` stores
line shifts relative to the beginning of the file, not
relative to the current line, which is inconvenient for
using `ReportLocation`.


Patch:
diff --git a/src/ir/location.go b/src/ir/location.go
new file mode 100644
index 0000000..ce36343
--- /dev/null
+++ b/src/ir/location.go
@@ -0,0 +1,20 @@
+package ir
+
+// Location stores the position of some element, where StartChar and EndChar
+// are offsets relative to the current line, as opposed to position.Position,
+// where the offset is relative to the beginning of the file.
+type Location struct {
+	StartLine int
+	EndLine   int
+	StartChar int
+	EndChar   int
+}
+
+func NewLocation(startLine int, endLine int, startChar int, endChar int) *Location {
+	return &Location{
+		StartLine: startLine,
+		EndLine:   endLine,
+		StartChar: startChar,
+		EndChar:   endChar,
+	}
+}
diff --git a/src/linter/custom.go b/src/linter/custom.go
index 0d07c21..11e2dee 100644
--- a/src/linter/custom.go
+++ b/src/linter/custom.go
@@ -139,14 +139,24 @@ func (ctx *RootContext) ParsePHPDoc(doc string) phpdoc.Comment {
 }
 
 // Report records linter warning of specified level.
-// chechName is a key that identifies the "checker" (diagnostic name) that found
+// checkName is a key that identifies the "checker" (diagnostic name) that found
 // issue being reported.
 func (ctx *RootContext) Report(n ir.Node, level int, checkName, msg string, args ...interface{}) {
 	ctx.w.Report(n, level, checkName, msg, args...)
 }
 
-func (ctx *RootContext) ReportByLine(lineNumber, level int, checkName, msg string, args ...interface{}) {
-	ctx.w.ReportByLine(lineNumber, level, checkName, msg, args...)
+// ReportPHPDoc records linter warning in PHPDoc of specified level.
+// checkName is a key that identifies the "checker" (diagnostic name) that found
+// issue being reported.
+func (ctx *RootContext) ReportPHPDoc(phpDocLocation PHPDocLocation, level int, checkName, msg string, args ...interface{}) {
+	ctx.w.ReportPHPDoc(phpDocLocation, level, checkName, msg, args...)
+}
+
+// ReportLocation records linter warning in specified location of specified level.
+// checkName is a key that identifies the "checker" (diagnostic name) that found
+// issue being reported.
+func (ctx *RootContext) ReportLocation(location ir.Location, level int, checkName, msg string, args ...interface{}) {
+	ctx.w.ReportLocation(location, level, checkName, msg, args...)
 }
 
 // Scope returns variables declared at root level.
@@ -199,8 +209,18 @@ func (ctx *BlockContext) Report(n ir.Node, level int, checkName, msg string, arg
 	ctx.w.r.Report(n, level, checkName, msg, args...)
 }
 
-func (ctx *BlockContext) ReportByLine(lineNumber, level int, checkName, msg string, args ...interface{}) {
-	ctx.w.r.ReportByLine(lineNumber, level, checkName, msg, args...)
+// ReportPHPDoc records linter warning in PHPDoc of specified level.
+// checkName is a key that identifies the "checker" (diagnostic name) that found
+// issue being reported.
+func (ctx *BlockContext) ReportPHPDoc(phpDocLocation PHPDocLocation, level int, checkName, msg string, args ...interface{}) {
+	ctx.w.r.ReportPHPDoc(phpDocLocation, level, checkName, msg, args...)
+}
+
+// ReportLocation records linter warning in specified location of specified level.
+// checkName is a key that identifies the "checker" (diagnostic name) that found
+// issue being reported.
+func (ctx *BlockContext) ReportLocation(location ir.Location, level int, checkName, msg string, args ...interface{}) {
+	ctx.w.r.ReportLocation(location, level, checkName, msg, args...)
 }
 
 // Scope returns variables declared in this block.
diff --git a/src/linter/phpdoc_util.go b/src/linter/phpdoc_util.go
index a248955..e56873f 100644
--- a/src/linter/phpdoc_util.go
+++ b/src/linter/phpdoc_util.go
@@ -17,11 +17,24 @@ type PHPDocLocation struct {
 	Line      int
 	Field     int
 	WholeLine bool
+	// RelativeLine is set if the line number is relative to the
+	// given node; otherwise, the line number is considered to be
+	// absolute and can be used directly.
+	RelativeLine bool
 }
 
 func PHPDocLine(n ir.Node, line int) PHPDocLocation {
 	return PHPDocLocation{
-		Node:      n,
+		Node:         n,
+		Line:         line,
+		WholeLine:    true,
+		RelativeLine: true,
+	}
+}
+
+func PHPDocAbsoluteLine(line int) PHPDocLocation {
+	return PHPDocLocation{
+		Node:      nil,
 		Line:      line,
 		WholeLine: true,
 	}
@@ -29,7 +42,16 @@ func PHPDocLine(n ir.Node, line int) PHPDocLocation {
 
 func PHPDocLineField(n ir.Node, line int, field int) PHPDocLocation {
 	return PHPDocLocation{
-		Node:  n,
+		Node:         n,
+		Line:         line,
+		Field:        field,
+		RelativeLine: true,
+	}
+}
+
+func PHPDocAbsoluteLineField(line int, field int) PHPDocLocation {
+	return PHPDocLocation{
+		Node:  nil,
 		Line:  line,
 		Field: field,
 	}
diff --git a/src/linter/root.go b/src/linter/root.go
index 6875b0d..b31f32c 100644
--- a/src/linter/root.go
+++ b/src/linter/root.go
@@ -119,8 +119,11 @@ func (d *rootWalker) handleCommentToken(t *token.Token) bool {
 				continue
 			}
 			if d.linterDisabled {
-				needleLine := ln.Line() + t.Position.StartLine - 1
-				d.ReportByLine(needleLine, LevelWarning, "linterError", "Linter is already disabled for this file")
+				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
@@ -129,8 +132,11 @@ func (d *rootWalker) handleCommentToken(t *token.Token) bool {
 			}
 			d.linterDisabled = canDisable
 			if !canDisable {
-				needleLine := ln.Line() + t.Position.StartLine - 1
-				d.ReportByLine(needleLine, LevelWarning, "linterError", "You are not allowed to disable linter")
+				needleLine := ln.Line() + t.Position.StartLine - 1 - 1
+				d.ReportPHPDoc(
+					PHPDocAbsoluteLineField(needleLine, 1),
+					LevelWarning, "linterError", "You are not allowed to disable linter",
+				)
 			}
 		}
 	}
@@ -309,147 +315,168 @@ func (d *rootWalker) parseStartPos(pos *position.Position) (startLn []byte, star
 	return startLn, startChar
 }
 
-func (d *rootWalker) report(n ir.Node, lineNumber int, phpDocLocation PHPDocLocation, level int, checkName, msg string, args ...interface{}) {
-	if !d.metaInfo().IsIndexingComplete() {
-		return
-	}
-	if d.file.AutoGenerated() && !d.config.CheckAutoGenerated {
-		return
+// 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
+		}
+
+		countPHPDocLines := strings.Count(doc, "\n") + 1
+
+		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
+		}
+
+		// 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
 	}
-	// We don't report anything if linter was disabled by a
-	// successful @linter disable, unless it's the linterError.
-	if d.linterDisabled && checkName != "linterError" {
+
+	if phpDocLocation.Line < 0 || phpDocLocation.Line >= d.file.NumLines() {
+		d.Report(phpDocLocation.Node, level, checkName, msg, args...)
 		return
 	}
 
-	if !d.checkersFilter.IsEnabledReport(checkName, d.ctx.st.CurrentFile) {
-		return
+	contextLine := d.file.Line(phpDocLocation.Line)
+
+	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, "/ *")
 	}
 
-	isReportForNode := phpDocLocation.Line == 0 && lineNumber == 0
-	isReportForLine := lineNumber != 0
-	isReportForPHPDoc := phpDocLocation.Line != 0
+	shiftFromStart := len(contextLine) - len(lineWithoutBeginning)
+
+	parts := bytes.Fields(lineWithoutBeginning)
+	if phpDocLocation.Field >= len(parts) {
+		phpDocLocation.Field = 0
+		phpDocLocation.WholeLine = true
+	}
 
-	var pos position.Position
-	var startLn []byte
 	var startChar int
-	var endLn []byte
 	var endChar int
 
-	switch {
-	case isReportForNode:
-		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 {
-			pos = *ir.GetPosition(n)
-		}
-
-		startLn, startChar = d.parseStartPos(&pos)
-
-		if pos.EndLine >= 1 && d.file.NumLines() > pos.EndLine {
-			endLn = d.file.Line(pos.EndLine - 1)
-			p := d.file.LinePosition(pos.EndLine - 1)
-			if pos.EndPos > p {
-				endChar = pos.EndPos - p
-			}
-		} else {
-			endLn = startLn
-		}
+	if phpDocLocation.WholeLine {
+		startChar = shiftFromStart
+		endChar = len(contextLine)
+	} else {
+		part := parts[phpDocLocation.Field]
+		shiftStart := bytes.Index(lineWithoutBeginning, part)
+		shiftEnd := shiftStart + len(part)
 
-		if endChar == 0 {
-			endChar = len(endLn)
-		}
-	case isReportForLine:
-		if lineNumber < 1 || lineNumber > d.file.NumLines() {
-			return
-		}
+		startChar = shiftFromStart + shiftStart
+		endChar = shiftFromStart + shiftEnd
+	}
 
-		startLn = d.file.Line(lineNumber - 1)
-		startChar = 0
+	if endChar == len(contextLine) && bytes.HasSuffix(contextLine, []byte("\r")) {
+		endChar--
+	}
 
-		endChar = len(startLn)
+	loc := ir.Location{
+		StartLine: phpDocLocation.Line,
+		EndLine:   phpDocLocation.Line,
+		StartChar: startChar,
+		EndChar:   endChar,
+	}
 
-		if strings.HasSuffix(string(startLn), "\r") {
-			endChar--
-		}
+	d.ReportLocation(loc, level, checkName, msg, args...)
+}
 
-		pos = position.Position{
-			StartLine: lineNumber,
-			EndLine:   lineNumber,
-		}
+func (d *rootWalker) Report(n ir.Node, level int, checkName, msg string, args ...interface{}) {
+	var pos position.Position
 
-	case isReportForPHPDoc:
-		countLines := 0
-		doc, ok := irutil.FindPhpDoc(phpDocLocation.Node, true)
-		if ok {
-			countLines = strings.Count(doc, "\n") + 1
+	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")
+				}
+			}
 		}
-
-		nodePos := ir.GetPosition(phpDocLocation.Node)
+	} else {
+		nodePos := ir.GetPosition(n)
 		if nodePos == nil {
-			nodePos = &position.Position{}
-		}
-		phpDocLocation.Line = nodePos.StartLine - (countLines - phpDocLocation.Line) - 1
-
-		if phpDocLocation.Line < 1 || phpDocLocation.Line > d.file.NumLines() {
 			return
 		}
+		pos = *nodePos
+	}
 
-		startLn = d.file.Line(phpDocLocation.Line - 1)
+	var loc ir.Location
 
-		lineWithoutBeginning := startLn
-		if !bytes.Contains(startLn, []byte("/*")) || bytes.Contains(startLn, []byte("/**")) {
-			lineWithoutBeginning = bytes.TrimLeft(startLn, "/ *")
-		}
+	loc.StartLine = pos.StartLine - 1
+	loc.EndLine = pos.EndLine - 1
+	loc.StartChar = pos.StartPos
+	loc.EndChar = pos.EndPos
 
-		shift := len(startLn) - len(lineWithoutBeginning)
+	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
+		}
+	}
 
-		parts := bytes.Fields(lineWithoutBeginning)
-		if phpDocLocation.Field >= len(parts) {
-			phpDocLocation.Field = 0
-			phpDocLocation.WholeLine = 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
 		}
+	}
 
-		if phpDocLocation.WholeLine {
-			startChar = shift
-			endChar = shift + len(lineWithoutBeginning)
-		} else {
-			part := parts[phpDocLocation.Field]
-			shiftStart := bytes.Index(lineWithoutBeginning, part)
-			shiftEnd := shiftStart + len(part)
+	d.ReportLocation(loc, level, checkName, msg, args...)
+}
 
-			startChar = shift + shiftStart
-			endChar = shift + shiftEnd
-		}
+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 endChar == len(startLn) && strings.HasSuffix(string(startLn), "\r") {
-			endChar--
-		}
+	if !d.checkersFilter.IsEnabledReport(checkName, d.ctx.st.CurrentFile) {
+		return
+	}
 
-		pos = position.Position{
-			StartLine: phpDocLocation.Line,
-			EndLine:   phpDocLocation.Line,
-		}
+	if loc.StartLine < 0 || loc.StartLine >= d.file.NumLines() {
+		return
 	}
 
-	msg = fmt.Sprintf(msg, args...)
+	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(&pos, startLn, checkName, msg)
+		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)
@@ -463,34 +490,19 @@ func (d *rootWalker) report(n ir.Node, lineNumber int, phpDocLocation PHPDocLoca
 
 	d.reports = append(d.reports, &Report{
 		CheckName: checkName,
-		Context:   string(startLn),
-		StartChar: startChar,
-		EndChar:   endChar,
-		Line:      pos.StartLine,
+		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:   msg,
+		Message:   fmt.Sprintf(msg, args...),
 		Hash:      hash,
 	})
 }
 
-// Report registers a single report message about some found problem.
-func (d *rootWalker) Report(n ir.Node, level int, checkName, msg string, args ...interface{}) {
-	d.report(n, 0, PHPDocLocation{}, 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{}) {
-	d.report(nil, lineNumber, PHPDocLocation{}, level, checkName, msg, args...)
-}
-
-// ReportPHPDoc registers a single report message about some found problem in PHPDoc.
-func (d *rootWalker) ReportPHPDoc(location PHPDocLocation, level int, checkName, msg string, args ...interface{}) {
-	d.report(nil, 0, location, 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 {
+// 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.
@@ -514,7 +526,7 @@ func (d *rootWalker) reportHash(pos *position.Position, startLine []byte, checkN
 	if !d.config.ConservativeBaseline {
 		// Lines are 1-based, indexes are 0-based.
 		// If this function is called, we expect that lines[index] exists.
-		index := pos.StartLine - 1
+		index := loc.StartLine - 1
 		if index >= 1 {
 			prevLine = d.file.Line(index - 1)
 		}
@@ -545,7 +557,7 @@ func (d *rootWalker) reportHash(pos *position.Position, startLine []byte, checkN
 	return baseline.ReportHash(&d.ctx.scratchBuf, baseline.HashFields{
 		Filename:  filename,
 		PrevLine:  bytes.TrimSuffix(prevLine, []byte("\r")),
-		StartLine: bytes.TrimSuffix(startLine, []byte("\r")),
+		StartLine: bytes.TrimSuffix(contextLine, []byte("\r")),
 		NextLine:  bytes.TrimSuffix(nextLine, []byte("\r")),
 		CheckName: checkName,
 		Message:   msg,


Test patch:
diff --git a/src/tests/golden/testdata/baseline-test/baseline.json b/src/tests/golden/testdata/baseline-test/baseline.json
index 6d61b24..cae6d42 100644
--- a/src/tests/golden/testdata/baseline-test/baseline.json
+++ b/src/tests/golden/testdata/baseline-test/baseline.json
@@ -1,26 +1,26 @@
 {
-  "LinterVersion": "",
-  "CreatedAt": 1595281530,
-  "Version": 3,
-  "Stats": {
-    "CountTotal": 3,
-    "CountPerCheck": {
-      "discardExpr": 1,
-      "undefined": 2
-    }
-  },
-  "Files": [
-    {
-      "File": "file1.php",
-      "Hashes": [
-        "oh8k7otr41kw,t3ecg0on789h"
-      ]
-    },
-    {
-      "File": "file2.php",
-      "Hashes": [
-        "2bbim48w63elq"
-      ]
-    }
-  ]
+	"LinterVersion": "0e47cc44623afb94af3d315708cf831696bbda9d",
+	"CreatedAt": 1626115955,
+	"Version": 3,
+	"Stats": {
+		"CountTotal": 3,
+		"CountPerCheck": {
+			"discardExpr": 1,
+			"undefined": 2
+		}
+	},
+	"Files": [
+		{
+			"File": "file1.php",
+			"Hashes": [
+				"11ngx7h7507kw,m3krbhb78pqm"
+			]
+		},
+		{
+			"File": "file2.php",
+			"Hashes": [
+				"209wyajyitniw"
+			]
+		}
+	]
 }
diff --git a/src/tests/golden/testdata/output-test/golden.txt b/src/tests/golden/testdata/output-test/golden.txt
index 13cfe4e..828ca9b 100644
--- a/src/tests/golden/testdata/output-test/golden.txt
+++ b/src/tests/golden/testdata/output-test/golden.txt
@@ -1,18 +1,21 @@
 WARNING linterError: You are not allowed to disable linter at testdata/output-test/output_test.php:3
 /** @linter disable */
-^^^^^^^^^^^^^^^^^^^^^^
+            ^^^^^^^
 WARNING linterError: You are not allowed to disable linter at testdata/output-test/output_test.php:6
  * @linter disable
-^^^^^^^^^^^^^^^^^^
+           ^^^^^^^
 WARNING linterError: You are not allowed to disable linter at testdata/output-test/output_test.php:10
  * @linter disable
-^^^^^^^^^^^^^^^^^^
+           ^^^^^^^
 WARNING linterError: You are not allowed to disable linter at testdata/output-test/output_test.php:11
  * @linter disable
-^^^^^^^^^^^^^^^^^^
+           ^^^^^^^
 WARNING linterError: You are not allowed to disable linter at testdata/output-test/output_test.php:12
  * @linter disable
-^^^^^^^^^^^^^^^^^^
+           ^^^^^^^
 WARNING linterError: You are not allowed to disable linter at testdata/output-test/output_test.php:13
  * @linter disable
-^^^^^^^^^^^^^^^^^^
+           ^^^^^^^
+MAYBE   arraySyntax: Use the short form '[]' instead of the old 'array()' at testdata/output-test/output_test.php:16
+$_ = array(); // warning in last line
+     ^^^^^^^
diff --git a/src/tests/golden/testdata/output-test/output_test.php b/src/tests/golden/testdata/output-test/output_test.php
index ccdf914..14fdff8 100644
--- a/src/tests/golden/testdata/output-test/output_test.php
+++ b/src/tests/golden/testdata/output-test/output_test.php
@@ -11,4 +11,6 @@
  * @linter disable
  * @linter disable
  * @linter disable
- */
\ No newline at end of file
+ */
+
+$_ = array(); // warning in last line
\ No newline at end of file