Skip to content
This repository was archived by the owner on Jan 30, 2023. It is now read-only.

Commit d767f4e

Browse files
author
Albert Meltzer
committed
Method length: count line which shares a comment
If there's code on a given line, in addition to a comment, let's count it.
1 parent babbdeb commit d767f4e

File tree

2 files changed

+161
-59
lines changed

2 files changed

+161
-59
lines changed

src/main/scala/org/scalastyle/scalariform/MethodLengthChecker.scala

Lines changed: 94 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616

1717
package org.scalastyle.scalariform
1818

19-
import _root_.scalariform.parser.FunDefOrDcl
20-
import org.scalastyle.CombinedAst
21-
import org.scalastyle.CombinedChecker
22-
import org.scalastyle.Lines
23-
import org.scalastyle.PositionError
24-
import org.scalastyle.ScalastyleError
19+
// scalastyle:off underscore.import
20+
import scala.annotation.tailrec
21+
22+
import _root_.scalariform.lexer._
23+
import _root_.scalariform.parser._
24+
25+
import org.scalastyle._
2526
import org.scalastyle.scalariform.VisitorHelper.Clazz
2627
import org.scalastyle.scalariform.VisitorHelper.visit
2728

@@ -38,75 +39,116 @@ class MethodLengthChecker extends CombinedChecker {
3839
case class FunDefOrDclClazz(t: FunDefOrDcl, position: Option[Int], subs: List[FunDefOrDclClazz])
3940
extends Clazz[FunDefOrDcl]()
4041

42+
@tailrec
43+
private def getInnerExpr(expr: Expr): ExprElement = expr.contents match {
44+
case List(subexpr) =>
45+
subexpr match {
46+
case t: Expr => getInnerExpr(t)
47+
case t: Any => t
48+
}
49+
case _ => expr
50+
}
51+
52+
private def getNextToken(tokens: List[Token], ignoreComments: Boolean): Option[Token] =
53+
tokens.tail.headOption match {
54+
case x @ Some(tok) if !ignoreComments =>
55+
tok.associatedWhitespaceAndComments.tokens.collectFirst { case t: Comment => t.token }.orElse(x)
56+
case x: Any => x
57+
}
58+
59+
private def getNextLastToken(tokens: List[Token], ignoreComments: Boolean): Option[Token] = {
60+
val lastTwoTokens = tokens.drop(tokens.length - 2)
61+
val lastComment = lastTwoTokens.lastOption match {
62+
case Some(tok) if !ignoreComments =>
63+
tok.associatedWhitespaceAndComments.tokens.findLast(_.isInstanceOf[Comment]).map(_.token)
64+
case _ => None
65+
}
66+
lastComment.orElse(lastTwoTokens.headOption)
67+
}
68+
4169
def verify(ast: CombinedAst): List[ScalastyleError] = {
4270
val maxLength = getInt("maxLength", DefaultMaximumLength)
4371
val ignoreComments = getBoolean("ignoreComments", DefaultIgnoreComments)
4472
val ignoreEmpty = getBoolean("ignoreEmpty", DefaultIgnoreEmpty)
4573

74+
val lines = ast.lines
4675
val it = for {
4776
t <- localvisit(ast.compilationUnit.immediateChildren.head)
4877
f <- traverse(t)
49-
if matches(f, ast.lines, maxLength, ignoreComments, ignoreEmpty)
50-
} yield PositionError(t.position.get, List("" + maxLength))
78+
body <- f.t.funBodyOpt match {
79+
case Some(t: ProcFunBody) => Some(t.bodyBlock)
80+
case Some(t: ExprFunBody) => Some(getInnerExpr(t.body))
81+
case _ => None
82+
}
83+
isBlock = body.isInstanceOf[BlockExpr]
84+
tokens = body.tokens
85+
headToken <- if (isBlock) getNextToken(tokens, ignoreComments) else tokens.headOption
86+
lastToken <- if (isBlock) getNextLastToken(tokens, ignoreComments) else tokens.lastOption
87+
begLineColumn <- lines.toLineColumn(headToken.offset)
88+
endLineColumn <- lines.toLineColumn(lastToken.lastCharacterOffset)
89+
numLines = methodLength(lines.lines, begLineColumn, endLineColumn, ignoreComments, ignoreEmpty)
90+
if numLines > maxLength
91+
} yield PositionError(t.position.get, List(s"$numLines > $maxLength"))
5192

5293
it
5394
}
5495

5596
private def traverse(t: FunDefOrDclClazz): List[FunDefOrDclClazz] = t :: t.subs.flatMap(traverse)
5697

57-
private def matches(
58-
t: FunDefOrDclClazz,
59-
lines: Lines,
60-
maxLines: Int,
98+
// scalastyle:off method.length cyclomatic.complexity
99+
private def methodLength(
100+
lines: Array[Line],
101+
begLineColumn: LineColumn, // included, 1-based
102+
endLineColumn: LineColumn, // included, 1-based
61103
ignoreComments: Boolean,
62104
ignoreEmpty: Boolean
63-
) = {
105+
): Int = {
106+
val begLine = begLineColumn.line - 1 // 0-based, included
64107
if (ignoreComments) {
65-
val count = for {
66-
(_, start) <- lines.findLineAndIndex(t.t.defToken.offset)
67-
(_, end) <- lines.findLineAndIndex(t.t.tokens.last.offset)
68-
} yield {
69-
var count = 0
70-
var multilineComment = false
71-
72-
// do not count deftoken line and end block line
73-
for (i <- (start + 1) until end) {
74-
val lineText =
75-
lines.lines(i - 1).text.trim // zero based index, therefore "-1" when accessing the line
76-
if (ignoreEmpty && lineText.isEmpty) {
77-
// do nothing
78-
} else if (lineText.startsWith(SinglelineComment)) {
79-
// do nothing
80-
} else {
81-
if (lineText.contains(MultilineCommentsOpener))
82-
// multiline comment start /*
83-
// this line won't be counted even if
84-
// there exists any token before /*
85-
multilineComment = true
86-
if (!multilineComment)
87-
count = count + 1
88-
if (lineText.contains(MultilineCommentsCloser))
89-
// multiline comment end */
90-
// this line won't be counted even if
91-
// there exists any token after */
92-
multilineComment = false
108+
var count = 0
109+
var multilineComment = false
110+
val endLine = endLineColumn.line - 1 // 0-based, included
111+
for {
112+
i <- begLine to endLine
113+
lineText = lines(i).text
114+
slcIndex = lineText.indexOf(SinglelineComment)
115+
if !(slcIndex == 0 || ignoreEmpty && lineText.isEmpty)
116+
} {
117+
val begIndex = if (i == begLine) begLineColumn.column else 0
118+
val endIndex = {
119+
val idx = if (slcIndex < 0) lineText.length else slcIndex
120+
if (i == endLine) math.min(endLineColumn.column + 1, idx) else idx
121+
}
122+
@tailrec
123+
def iter(index: Int, res: Boolean = false): Boolean = {
124+
val delim = if (multilineComment) MultilineCommentsCloser else MultilineCommentsOpener
125+
val nextIndex = {
126+
val idx = lineText.indexOf(delim, index)
127+
if (idx < 0 || idx > endIndex) endIndex else idx
128+
}
129+
val nextRes = res || !multilineComment && {
130+
val idx = lineText.indexWhere(!Character.isSpaceChar(_), index)
131+
idx >= 0 && idx < nextIndex
132+
}
133+
if (nextIndex == endIndex) { nextRes }
134+
else {
135+
multilineComment = !multilineComment
136+
iter(nextIndex + delim.length, nextRes)
93137
}
94138
}
95-
count
139+
if (iter(begIndex)) count += 1
96140
}
97-
count.get > maxLines
141+
count
98142
} else {
99-
val head = lines.toLine(t.t.defToken.offset).get + 1
100-
val tail = lines.toLine(t.t.tokens.last.offset).get - 1
101-
val emptyLines = if (ignoreEmpty) {
102-
lines.lines
103-
.slice(head - 1, tail) // extract actual method content
104-
.count(_.text.isEmpty) // count empty lines
105-
} else
106-
0
107-
(tail - head + 1) - emptyLines > maxLines
143+
val endLine = endLineColumn.line // 0-based, excluded
144+
if (ignoreEmpty) {
145+
lines.view.slice(begLine, endLine).count(_.text.nonEmpty)
146+
} else {
147+
endLine - begLine
148+
}
108149
}
109150
}
151+
// scalastyle:on method.length
110152

111153
private def localvisit(ast: Any): List[FunDefOrDclClazz] =
112154
ast match {

src/test/scala/org/scalastyle/scalariform/MethodLengthCheckerTest.scala

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,23 @@ class F2() {
8888
4 (7)
8989
5 (8)
9090
}
91+
def method3() = {
92+
1
93+
2
94+
/** (3)
95+
* (4)
96+
*/ (5)
97+
3 (6)
98+
4 (7)
99+
}
91100
}
92101
"""
93102

94-
assertErrors(List(), source, Map("maxLength" -> "5", "ignoreComments" -> "true"))
103+
assertErrors(
104+
List(columnError(5, 6, List("6 > 5"))),
105+
source,
106+
Map("maxLength" -> "5", "ignoreComments" -> "true")
107+
)
95108
}
96109

97110
@Test def testIgnoreEmpty(): Unit = {
@@ -140,7 +153,11 @@ class F2() {
140153
}
141154
"""
142155

143-
assertErrors(List(columnError(5, 6, List("4"))), source, Map("maxLength" -> "4", "ignoreEmpty" -> "true"))
156+
assertErrors(
157+
List(columnError(5, 6, List("5 > 4"))),
158+
source,
159+
Map("maxLength" -> "4", "ignoreEmpty" -> "true")
160+
)
144161
}
145162

146163
@Test def testIgnoreEmptyAndComments(): Unit = {
@@ -173,10 +190,29 @@ class F2() {
173190
5 (12)
174191
175192
}
193+
def method3() = {
194+
195+
1 (2)
196+
2 (3)
197+
// (4)
198+
199+
3 (6)
200+
4 (7)
201+
/** (8)
202+
* (9)
203+
*/
204+
205+
5 (12)
206+
207+
}
176208
}
177209
"""
178210

179-
assertErrors(List(), source, Map("maxLength" -> "5", "ignoreComments" -> "true", "ignoreEmpty" -> "true"))
211+
assertErrors(
212+
List(columnError(14, 6, List("6 > 5"))),
213+
source,
214+
Map("maxLength" -> "5", "ignoreComments" -> "true", "ignoreEmpty" -> "true")
215+
)
180216
}
181217

182218
@Test def testNotIgnoreComments(): Unit = {
@@ -205,7 +241,7 @@ class F2() {
205241
"""
206242

207243
assertErrors(
208-
List(columnError(5, 6, List("5")), columnError(13, 6, List("5"))),
244+
List(columnError(5, 6, List("6 > 5")), columnError(13, 6, List("6 > 5"))),
209245
source,
210246
Map("maxLength" -> "5", "ignoreComments" -> "false")
211247
)
@@ -214,6 +250,7 @@ class F2() {
214250
@Test def testIgnoreCommentsComplicated(): Unit = {
215251
val source = """
216252
class F3() {
253+
217254
def method1() = {
218255
1 //
219256
(2) /* */
@@ -222,9 +259,8 @@ class F3() {
222259
(4) /*
223260
* (5)
224261
*/ (6)
225-
4 (7)
226-
5 (8)
227262
}
263+
228264
def method2() = {
229265
// /* (1)
230266
1 (2)
@@ -234,10 +270,34 @@ class F3() {
234270
5 (7)
235271
6 (8)
236272
}
273+
274+
def method3() = {
275+
1 //
276+
(2) /* */
277+
2
278+
3
279+
(4) /*
280+
* (5)
281+
*/ (6)
282+
4 (7)
283+
}
284+
285+
def method4() = {
286+
// /* (1)
287+
1 (2)
288+
2 (3)
289+
3 (5)
290+
4 (6)
291+
5 (7)
292+
}
237293
}
238294
"""
239295
assertErrors(
240-
List(columnError(14, 6, List("5"))),
296+
List(
297+
columnError(4, 6, List("6 > 5")),
298+
columnError(14, 6, List("6 > 5")),
299+
columnError(24, 6, List("7 > 5"))
300+
),
241301
source,
242302
Map("maxLength" -> "5", "ignoreComments" -> "true")
243303
)

0 commit comments

Comments
 (0)