1616
1717package 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 ._
2526import org .scalastyle .scalariform .VisitorHelper .Clazz
2627import 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 {
0 commit comments