Skip to content

Commit bc4cc6c

Browse files
authored
Merge pull request #341 from olafurpg/moar-lints
Fix linter issues discovered while writing scala-lang blogpost.
2 parents 906be5e + 79dc280 commit bc4cc6c

File tree

8 files changed

+119
-55
lines changed

8 files changed

+119
-55
lines changed

scalafix-core/shared/src/main/scala/org/langmeta/internal/ScalafixLangmetaHacks.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ object ScalafixLangmetaHacks {
1717
val count = end - start
1818
val eolOffset =
1919
if (input.chars.lift(start + count - 1).contains('\n')) -1 else 0
20-
new String(input.chars, start, count + eolOffset)
20+
new String(input.chars, start, math.max(0, count + eolOffset))
2121
}
22-
var caret = " " * pos.startColumn + "^"
22+
val caret = " " * pos.startColumn + "^"
2323
header + EOL + line + EOL + caret
2424
} else {
2525
s"$severity: $message"

scalafix-core/shared/src/main/scala/scalafix/internal/rule/NoInfer.scala

-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ case object NoInfer {
3636
Symbol("_root_.java.io.Serializable#"),
3737
Symbol("_root_.scala.Any#"),
3838
Symbol("_root_.scala.AnyVal#"),
39-
Symbol("_root_.scala.AnyVal#"),
4039
Symbol("_root_.scala.Product#")
4140
)
4241

scalafix-core/shared/src/main/scala/scalafix/lint/LintCategory.scala

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ final case class LintCategory(
1919
) {
2020
def key(owner: RuleName): String =
2121
if (owner.isEmpty) id
22+
else if (id.isEmpty) owner.value
2223
else s"${owner.value}.$id"
2324
private def noExplanation: LintCategory =
2425
new LintCategory(id, explanation, severity)

scalafix-core/shared/src/main/scala/scalafix/patch/Patch.scala

+25-6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import scalafix.internal.util.Failure
1515
import scalafix.internal.util.TokenOps
1616
import scalafix.lint.LintMessage
1717
import scalafix.patch.TreePatch.ReplaceSymbol
18+
import scalafix.rule.RuleName
1819
import org.scalameta.logger
1920

2021
/** A data structure that can produce a .patch file.
@@ -116,13 +117,20 @@ object Patch {
116117
case _ => throw Failure.TokenPatchMergeError(a, b)
117118
}
118119

119-
private[scalafix] def lintMessages(
120-
patch: Patch,
121-
ctx: RuleCtx,
122-
checkMessages: scala.Seq[LintMessage]
123-
): List[LintMessage] = {
120+
private[scalafix] def reportLintMessages(
121+
patches: Map[RuleName, Patch],
122+
ctx: RuleCtx): Unit = {
123+
patches.foreach {
124+
case (name, patch) =>
125+
Patch.lintMessages(patch).foreach { msg =>
126+
// Set the lint message owner. This allows us to distinguish
127+
// LintCategory with the same id from different rules.
128+
ctx.printLintMessage(msg, name)
129+
}
130+
}
131+
}
132+
private[scalafix] def lintMessages(patch: Patch): List[LintMessage] = {
124133
val builder = List.newBuilder[LintMessage]
125-
checkMessages.foreach(builder += _)
126134
foreach(patch) {
127135
case LintPatch(lint) =>
128136
builder += lint
@@ -200,6 +208,17 @@ object Patch {
200208
builder.result()
201209
}
202210

211+
private[scalafix] def isOnlyLintMessages(patch: Patch): Boolean = {
212+
// TODO(olafur): foreach should really return Stream[Patch] for early termination.
213+
var onlyLint = true
214+
var hasLintMessage = false
215+
foreach(patch) {
216+
case _: LintPatch => hasLintMessage = true
217+
case _ => onlyLint = false
218+
}
219+
hasLintMessage && onlyLint
220+
}
221+
203222
private def foreach(patch: Patch)(f: Patch => Unit): Unit = {
204223
def loop(patch: Patch): Unit = patch match {
205224
case Concat(a, b) =>

scalafix-core/shared/src/main/scala/scalafix/rule/Rule.scala

+21-9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import scalafix.syntax._
99
import metaconfig.Conf
1010
import metaconfig.ConfDecoder
1111
import metaconfig.Configured
12+
import org.scalameta.logger
1213

1314
/** A Scalafix Rule.
1415
*
@@ -79,13 +80,11 @@ abstract class Rule(ruleName: RuleName) { self =>
7980
}
8081
final def apply(input: String): String = apply(Input.String(input))
8182
final def apply(ctx: RuleCtx, patch: Patch): String = {
82-
val result = Patch(patch, ctx, semanticOption)
83-
val checkMessages = check(ctx)
84-
Patch.lintMessages(patch, ctx, checkMessages).foreach { msg =>
85-
// Set the lint message owner. This allows us to distinguish
86-
// LintCategory with the same id from different rules.
87-
ctx.printLintMessage(msg, name)
88-
}
83+
apply(ctx, Map(name -> patch))
84+
}
85+
final def apply(ctx: RuleCtx, patches: Map[RuleName, Patch]): String = {
86+
val result = Patch(patches.values.asPatch, ctx, semanticOption)
87+
Patch.reportLintMessages(patches, ctx)
8988
result
9089
}
9190

@@ -101,6 +100,9 @@ abstract class Rule(ruleName: RuleName) { self =>
101100

102101
private[scalafix] final def allNames: List[String] =
103102
name.identifiers.map(_.value)
103+
protected[scalafix] def fixWithName(ctx: RuleCtx): Map[RuleName, Patch] =
104+
Map(name -> (fix(ctx) ++ check(ctx).map(ctx.lint)))
105+
104106
final override def toString: String = name.toString
105107
final def name: RuleName = ruleName
106108
// NOTE. This is kind of hacky and hopefully we can find a better workaround.
@@ -132,6 +134,8 @@ object Rule {
132134
}
133135
override def check(ctx: RuleCtx): Seq[LintMessage] =
134136
rules.flatMap(_.check(ctx))
137+
override def fixWithName(ctx: RuleCtx): Map[RuleName, Patch] =
138+
rules.foldLeft(Map.empty[RuleName, Patch])(_ ++ _.fixWithName(ctx))
135139
override def fix(ctx: RuleCtx): Patch =
136140
Patch.empty ++ rules.map(_.fix(ctx))
137141
override def semanticOption: Option[SemanticdbIndex] =
@@ -181,6 +185,14 @@ object Rule {
181185
}
182186

183187
/** Combine two rules into a single rule */
184-
def merge(a: Rule, b: Rule): Rule =
185-
new CompositeRule(a :: b :: Nil)
188+
def merge(a: Rule, b: Rule): Rule = (a, b) match {
189+
case (c1: CompositeRule, c2: CompositeRule) =>
190+
new CompositeRule(c1.rules ::: c2.rules)
191+
case (c1: CompositeRule, _) =>
192+
new CompositeRule(b :: c1.rules)
193+
case (_, c2: CompositeRule) =>
194+
new CompositeRule(b :: c2.rules)
195+
case (_, _) =>
196+
new CompositeRule(a :: b :: Nil)
197+
}
186198
}

scalafix-testkit/src/main/scala/scalafix/testkit/SemanticRuleSuite.scala

+45-37
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import org.scalatest.BeforeAndAfterAll
1010
import org.scalatest.FunSuite
1111
import org.scalatest.exceptions.TestFailedException
1212
import scala.util.matching.Regex
13+
import scalafix.rule.RuleName
1314
import org.langmeta.internal.ScalafixLangmetaHacks
1415

1516
object SemanticRuleSuite {
@@ -65,61 +66,70 @@ abstract class SemanticRuleSuite(
6566
private def assertLintMessagesAreReported(
6667
rule: Rule,
6768
ctx: RuleCtx,
68-
lints: List[LintMessage],
69+
patches: Map[RuleName, Patch],
6970
tokens: Tokens): Unit = {
70-
val lintMessages = lints.to[mutable.Set]
71-
def assertLint(position: Position, key: String): Unit = {
72-
val matchingMessage = lintMessages.filter { m =>
73-
assert(m.position.input == position.input)
74-
m.position.startLine == position.startLine &&
75-
m.category.key(rule.name) == key
76-
}
77-
if (matchingMessage.isEmpty) {
71+
72+
type Msg = (Position, String)
73+
74+
def matches(a: Msg)(b: Msg) =
75+
a._1.startLine == b._1.startLine &&
76+
a._2 == b._2
77+
78+
def diff(a: Seq[Msg], b: Seq[Msg]) =
79+
a.filter(x => !b.exists(matches(x)))
80+
81+
val lintAssertions = tokens.collect {
82+
case tok @ Token.Comment(SemanticRuleSuite.LintAssertion(key)) =>
83+
tok.pos -> key
84+
}
85+
val lintMessages = patches.toSeq.flatMap {
86+
case (name, patch) =>
87+
Patch
88+
.lintMessages(patch)
89+
.map(lint => lint.position -> lint.category.key(name))
90+
}
91+
92+
val uncoveredAsserts = diff(lintAssertions, lintMessages)
93+
uncoveredAsserts.foreach {
94+
case (pos, key) =>
7895
throw new TestFailedException(
7996
ScalafixLangmetaHacks.formatMessage(
80-
position,
97+
pos,
8198
"error",
8299
s"Message '$key' was not reported here!"),
83100
0
84101
)
85-
} else {
86-
lintMessages --= matchingMessage
87-
}
88102
}
89-
tokens.foreach {
90-
case tok @ Token.Comment(SemanticRuleSuite.LintAssertion(key)) =>
91-
assertLint(tok.pos, key)
92-
case _ =>
93-
}
94-
if (lintMessages.nonEmpty) {
95-
lintMessages.foreach(x => ctx.printLintMessage(x, rule.name))
96-
val key = lintMessages.head.category.key(rule.name)
97-
val explanation =
98-
s"""|To fix this problem, suffix the culprit lines with
99-
| // assert: $key
100-
|""".stripMargin
103+
104+
val uncoveredMessages = diff(lintMessages, lintAssertions)
105+
if (uncoveredMessages.nonEmpty) {
106+
Patch.reportLintMessages(patches, ctx)
107+
val explanation = uncoveredMessages
108+
.groupBy(_._2)
109+
.map {
110+
case (key, positions) =>
111+
s"""Append to lines: ${positions.map(_._1.startLine).mkString(", ")}
112+
| // assert: $key""".stripMargin
113+
}
114+
.mkString("\n\n")
101115
throw new TestFailedException(
102-
s"Uncaught linter messages! $explanation",
116+
s"Uncaught linter messages! To fix this problem\n$explanation",
103117
0)
104118
}
105119
}
106120

107121
def runOn(diffTest: DiffTest): Unit = {
108122
test(diffTest.name) {
109123
val (rule, config) = diffTest.config.apply()
110-
val ctx = RuleCtx(
124+
val ctx: RuleCtx = RuleCtx(
111125
config.dialect(diffTest.original).parse[Source].get,
112126
config.copy(dialect = diffTest.document.dialect)
113127
)
114-
val patch = rule.fix(ctx)
128+
val patches = rule.fixWithName(ctx)
129+
assertLintMessagesAreReported(rule, ctx, patches, ctx.tokens)
130+
val patch = patches.values.asPatch
115131
val obtainedWithComment = Patch.apply(patch, ctx, rule.semanticOption)
116132
val tokens = obtainedWithComment.tokenize.get
117-
val checkMessages = rule.check(ctx)
118-
assertLintMessagesAreReported(
119-
rule,
120-
ctx,
121-
Patch.lintMessages(patch, ctx, checkMessages),
122-
ctx.tokens)
123133
val obtained = SemanticRuleSuite.stripTestkitComments(tokens)
124134
val candidateOutputFiles = expectedOutputSourceroot.flatMap { root =>
125135
val scalaSpecificFilename =
@@ -132,9 +142,7 @@ abstract class SemanticRuleSuite(
132142
.collectFirst { case f if f.isFile => f.readAllBytes }
133143
.map(new String(_))
134144
.getOrElse {
135-
// TODO(olafur) come up with more principled check to determine if
136-
// rule is linter or rewrite.
137-
if (checkMessages.nonEmpty) obtained // linter
145+
if (Patch.isOnlyLintMessages(patch)) obtained // linter
138146
else {
139147
val tried = candidateOutputFiles.mkString("\n")
140148
sys.error(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
rules = [
3+
NoInfer
4+
"class:scalafix.test.DummyLinter"
5+
]
6+
*/
7+
package test
8+
9+
class LintAsserts {
10+
List(1, (1, 2)) // assert: NoInfer.any
11+
}
12+
// assert: DummyLinter
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package scalafix.test
2+
3+
import scalafix.LintCategory
4+
import scalafix.LintMessage
5+
import scalafix.Rule
6+
import scalafix.RuleCtx
7+
8+
object DummyLinter extends Rule("DummyLinter") {
9+
val error = LintCategory.error("Bam!")
10+
override def check(ctx: RuleCtx): Seq[LintMessage] = {
11+
error.at(ctx.tokenList.prev(ctx.tokens.last).pos) :: Nil
12+
}
13+
}

0 commit comments

Comments
 (0)